<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="../assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>TinyComputers.io (Posts about retrocomputing)</title><link>https://tinycomputers.io/</link><description></description><atom:link href="https://tinycomputers.io/categories/cat_retrocomputing.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><copyright>Contents © 2026 A.C. Jokela 
&lt;!-- div style="width: 100%" --&gt;
&lt;a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/"&gt;&lt;img alt="" style="border-width:0" src="https://i.creativecommons.org/l/by-sa/4.0/80x15.png" /&gt; Creative Commons Attribution-ShareAlike&lt;/a&gt;&amp;nbsp;|&amp;nbsp;
&lt;!-- /div --&gt;
</copyright><lastBuildDate>Tue, 07 Apr 2026 13:45:53 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>Designing a Dual Z80 RetroShield: Ground Planes, Ghost Shorts, and the Fix (Part 2)</title><link>https://tinycomputers.io/posts/designing-a-dual-z80-retroshield-part-2.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/designing-a-dual-z80-retroshield-part-2_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;29 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;div style="float: right; max-width: 480px; margin: 0 0 1em 1.5em;"&gt;
&lt;img src="https://tinycomputers.io/images/dual-z80/IMG_4436.jpeg" alt="The assembled dual Z80 RetroShield plugged into an Arduino Mega 2560, with colored jumper wires running from the J2 control header to the Mega's free digital pins" style="width: 100%; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);"&gt;
&lt;em style="display: block; font-size: 0.85em; margin-top: 0.4em;"&gt;The assembled dual Z80 RetroShield on the bench. Two Z80 CPUs socketed, jumper wires from J2 to the Arduino Mega's free pins, ready for testing.&lt;/em&gt;
&lt;/div&gt;

&lt;p&gt;In &lt;a href="https://tinycomputers.io/posts/designing-a-dual-z80-retroshield-part-1.html"&gt;Part 1&lt;/a&gt;, I designed a dual-Z80 RetroShield PCB entirely from the command line: two Z80 CPUs sharing an address and data bus, with independent control signals on a supplementary header. The Gerber files went to PCBWay. The boards arrived. I soldered everything up, plugged the shield into an &lt;a href="https://baud.rs/CKQf4B"&gt;Arduino Mega&lt;/a&gt;, wired jumpers from the J2 control header to the Mega's free pins, loaded a test sketch, and...&lt;/p&gt;
&lt;p&gt;Nothing. Both Z80s appeared to be alive (the diagnostic showed bus activity after reset), but they couldn't execute a single instruction. The data bus was completely unresponsive. The SMP kernel I'd written—a 52-byte symmetric multiprocessing demo where both CPUs boot the same code, pull tasks from a shared scheduler, and sum arrays in parallel—hit its cycle limit and returned zeroes.&lt;/p&gt;
&lt;p&gt;What followed was a multi-day debugging session that taught me more about PCB design than the entire design phase did. The root cause turned out to be a subtle interaction between pcb-rnd's ground fill polygon and its Gerber exporter. This is the story of finding it.&lt;/p&gt;
&lt;h3&gt;The Hardware&lt;/h3&gt;
&lt;p&gt;The boards came back from PCBWay with the usual four week turn around time. Clean fabrication, good silkscreen, no obvious defects on visual inspection. I soldered &lt;a href="https://baud.rs/pcKTdF"&gt;DIP-40 sockets&lt;/a&gt; for both &lt;a href="https://baud.rs/CJA3JT"&gt;Z80s&lt;/a&gt;, the 2×18 J1 bus header, the 2×6 J2 control header, and the bus activity LED. The Z80 chips dropped into their sockets with satisfying precision.&lt;/p&gt;
&lt;p&gt;The J2 header needed &lt;a href="https://baud.rs/eiPjaE"&gt;jumper wires&lt;/a&gt; to the Arduino Mega's free pins (D0–D21). I chose a deliberate mapping based on the pin functions: D9 for CPU2's clock (Timer1 OC1A, which can generate a hardware PWM signal for a stable 4 MHz clock), D4 for RESET, D5–D6 for INT/NMI, D7–D8 for MREQ/IORQ, D10–D11 for RD/WR, and D12–D13 for BUSRQ/BUSAK. Twelve jumper wires total, plus +5V and GND.&lt;/p&gt;
&lt;p&gt;The plan was to run a five-test validation suite: each Z80 solo (write a signature byte to a known address), shared RAM persistence (both CPUs write to different locations, verify both persist), a relay test (CPU1 computes a value, CPU2 picks it up and continues), and a loop counter (DJNZ loop to verify branch instructions work). After that, the SMP kernel.&lt;/p&gt;
&lt;p&gt;None of it worked.&lt;/p&gt;
&lt;video controls style="width: 100%; max-width: 640px; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); margin: 1.5em 0; margin-bottom: -1em;"&gt;
&lt;source src="https://tinycomputers.io/images/dual-z80/dual-z80-retroshield.mp4" type="video/mp4"&gt;
&lt;/source&gt;&lt;/video&gt;
&lt;p&gt;&lt;em&gt;Routing traces with Freerouting.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;First Contact: The Diagnostic Sketch&lt;/h3&gt;
&lt;p&gt;I backed off to a simpler diagnostic sketch to test each connection individually. It checked four things: idle state of control pins, bus activity after releasing reset, address bus bit toggling, and the BUSRQ/BUSAK handshake on CPU2.&lt;/p&gt;
&lt;p&gt;The results were a mix of encouraging and confusing:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="o"&gt;===&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Control&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Pin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Idle&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;===&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FAIL&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;U1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;MREQ&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;D41&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;idle&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HIGH&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FAIL&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;U1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;IORQ&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;D39&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;idle&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HIGH&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FAIL&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;U1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RD&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;D53&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;idle&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HIGH&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FAIL&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;U1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;WR&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;D40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;idle&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HIGH&lt;/span&gt;

&lt;span class="o"&gt;===&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;U1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Bus&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Activity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;===&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PASS&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;MREQ&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;went&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LOW&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Z80&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;alive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PASS&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;went&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LOW&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Z80&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fetching&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PASS&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bus&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;first&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;fetch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x0000&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Both Z80s were "alive" in the sense that they responded to clock pulses and attempted to fetch from address 0x0000 after reset. But every control signal (MREQ, IORQ, RD, WR) read LOW regardless of what the Z80 was doing. They should have been toggling between HIGH and LOW during bus cycles. LOW all the time meant either the Z80 wasn't actually driving these pins, or something else was pulling them down.&lt;/p&gt;
&lt;p&gt;I filed this under "weird but not fatal" and pushed ahead to the SMP test. That's when things got serious.&lt;/p&gt;
&lt;h3&gt;The Bus Trace That Went Nowhere&lt;/h3&gt;
&lt;p&gt;The SMP kernel loaded into emulated memory at address 0x0000. After releasing reset, the Z80 should have fetched its first instruction (0xDB, the opcode for &lt;code&gt;IN A, (n)&lt;/code&gt;), executed it, and proceeded through the scheduler loop. Instead, a 150-cycle bus trace showed this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;Cyc  MREQ IORQ RD WR  Addr    Data  Action
  0  LOW   LOW   L  L  0x0000  0xDB  MEM RD
  1  LOW   LOW   L  L  0x0000  0xDB  MEM RD
  2  LOW   LOW   L  L  0x0000  0xDB  MEM RD
  ...
149  LOW   LOW   L  L  0x0000  0xDB  MEM RD
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Every single cycle: same address, same data, same control signals. The Z80 was stuck at its reset vector, endlessly attempting to fetch the first byte and never advancing. MREQ and IORQ were LOW simultaneously on every cycle, which should never happen during normal Z80 operation—they're mutually exclusive signals.&lt;/p&gt;
&lt;p&gt;The Z80 was putting 0x0000 on the address bus (correct for a reset vector fetch), and I was driving 0xDB on the data bus (the correct opcode). But the Z80 wasn't reading it. Or rather, it was reading something else.&lt;/p&gt;
&lt;h3&gt;The Data Bus Loopback Test&lt;/h3&gt;
&lt;p&gt;I added a simple test: with the Z80 held in reset (outputs tri-stated), drive patterns on the data bus and read them back. If the Arduino writes 0xFF to PORTL and reads back 0xFF from PINL, the data bus is clean. If it reads back something else, there's a short or broken trace.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gd"&gt;--- Data Bus Drive Test ---&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt; bit0(D49)  wrote 0x01 read 0x01 OK
&lt;span class="w"&gt; &lt;/span&gt; bit1(D48)  wrote 0x02 read 0x02 OK
&lt;span class="w"&gt; &lt;/span&gt; bit2(D47)  wrote 0x04 read 0x04 OK
&lt;span class="w"&gt; &lt;/span&gt; bit3(D46)  wrote 0x08 read 0x08 OK
&lt;span class="w"&gt; &lt;/span&gt; bit4(D45)  wrote 0x10 read 0x10 OK
&lt;span class="w"&gt; &lt;/span&gt; bit5(D44)  wrote 0x20 read 0x00 FAIL
&lt;span class="w"&gt; &lt;/span&gt; bit6(D43)  wrote 0x40 read 0x00 FAIL
&lt;span class="w"&gt; &lt;/span&gt; bit7(D42)  wrote 0x80 read 0x80 OK
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Bits 5 and 6 were stuck LOW. The Arduino's GPIO pins couldn't drive them HIGH—something on the board was pulling those lines to ground with enough strength to overpower the Mega's output drivers.&lt;/p&gt;
&lt;p&gt;This explained why the Z80 couldn't execute. When I drove 0xDB (&lt;code&gt;IN A, (n)&lt;/code&gt;) on the data bus, the Z80 actually saw 0x9B (bits 5 and 6 forced low), which decodes as &lt;code&gt;SBC A, E&lt;/code&gt;—a completely different instruction. The Z80 was faithfully executing garbage.&lt;/p&gt;
&lt;h3&gt;Isolating the Short&lt;/h3&gt;
&lt;p&gt;Systematic isolation. First question: is it the Arduino or the board?&lt;/p&gt;
&lt;p&gt;I pulled the RetroShield off the Mega and ran the same loopback test with the shield disconnected. Every bit passed perfectly. The Arduino's PORTL pins (D42–D49) could drive any pattern and read it back correctly. The problem was definitively on the board.&lt;/p&gt;
&lt;p&gt;Second question: is it CPU1 or CPU2? The two Z80s share the address and data bus, so a fault on either side would affect both. I disconnected J2's +5V jumper to deprive CPU2 of power, leaving its outputs floating. Same result—bits 5 and 6 still stuck LOW. So CPU2 wasn't the culprit. The short was in CPU1's territory.&lt;/p&gt;
&lt;p&gt;Third question: is it the Z80 chip or the PCB? I pulled U1 from its socket. Bits 5 and 6 still stuck. Pulled U2 as well (since it shares the bus traces even without power). Both chips out, empty sockets, and the shorts persisted.&lt;/p&gt;
&lt;p&gt;Then I ran a comprehensive pin test with both Z80 chips removed—just the bare PCB with sockets:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gd"&gt;--- Data Bus (PORTL) ---&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt; [SHORT] D0/bit0 (D49/PL0)
&lt;span class="w"&gt; &lt;/span&gt; [SHORT] D1/bit1 (D48/PL1)
&lt;span class="w"&gt; &lt;/span&gt; [SHORT] D2/bit2 (D47/PL2)
&lt;span class="w"&gt; &lt;/span&gt; [SHORT] D3/bit3 (D46/PL3)
&lt;span class="w"&gt; &lt;/span&gt; [SHORT] D4/bit4 (D45/PL4)
&lt;span class="w"&gt; &lt;/span&gt; [SHORT] D5/bit5 (D44/PL5)
&lt;span class="w"&gt; &lt;/span&gt; [SHORT] D6/bit6 (D43/PL6)
&lt;span class="w"&gt; &lt;/span&gt; [SHORT] D7/bit7 (D42/PL7)

&lt;span class="gd"&gt;--- Address Bus ---&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt; [SHORT] A0 through A15 — all 16 lines

&lt;span class="gd"&gt;--- U1 Control Pins ---&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt; [OK] MREQ, IORQ, RD, WR, RESET, INT, NMI, CLK — all 8 fine
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Every single data line. Every single address line. All 24 shorted to ground. But all 8 control signals were clean.&lt;/p&gt;
&lt;p&gt;This wasn't random solder bridges. The pattern was too systematic: every line that belonged to the shared bus (connected to both U1 and U2) was shorted, while every line that connected to only one Z80 was fine. Something structural was wrong with the PCB.&lt;/p&gt;
&lt;h3&gt;The Ground Fill&lt;/h3&gt;
&lt;p&gt;I went back to the PCB design files. The original RetroShield design included a copper fill polygon on the bottom layer—a ground plane covering roughly the left half of the board (x = 0.3mm to 55.6mm, y = 0.3mm to 53.1mm). This is standard practice: ground planes reduce noise, improve signal integrity, and provide a low-impedance return path for high-frequency signals.&lt;/p&gt;
&lt;p&gt;The polygon had a &lt;code&gt;clearpoly&lt;/code&gt; flag, which tells pcb-rnd to maintain clearance around pins that aren't connected to the fill. Each Z80 through-hole pin specified 0.762mm clearance. The fill should have maintained that gap around every signal pin, connecting only to GND pins (via thermal relief pads) and leaving all data and address pins isolated.&lt;/p&gt;
&lt;p&gt;I also found some design-level flag errors. U1 pin 1 (A11, an address line) had a &lt;code&gt;thermal(0X)&lt;/code&gt; flag—explicitly telling pcb-rnd to connect this signal pin to a copper fill on the top layer. Several +5V pins had &lt;code&gt;connected&lt;/code&gt; flags. These were wrong, though in pcb-rnd's net-aware polygon system, they turned out to be harmless (a &lt;code&gt;connected&lt;/code&gt; flag only connects a pin to a fill on the same net, so +5V pins wouldn't connect to a GND fill). I fixed them anyway.&lt;/p&gt;
&lt;p&gt;But fixing the flags didn't solve the short. The 24 bus lines were still shorted to ground with both chips removed. The problem was deeper.&lt;/p&gt;
&lt;h3&gt;The Gerber Analysis&lt;/h3&gt;
&lt;p&gt;I dug into the actual Gerber output for the bottom copper layer. In Gerber format, ground fill clearances are typically achieved either through layer polarity commands (&lt;code&gt;%LPC*%&lt;/code&gt; to switch to "clear" mode and punch out holes) or by drawing the fill as a region with the clearance areas built into its outline.&lt;/p&gt;
&lt;p&gt;pcb-rnd uses the region approach. The fill polygon gets exported as a complex region (G36/G37 block) whose boundary weaves around each pin, creating clearance cutouts. Or at least, that's what it's supposed to do.&lt;/p&gt;
&lt;p&gt;I wrote a script to analyze the region vertices near U1's pins. For pin 9 (D5, a signal pin that should have full clearance), this is what the Gerber contained:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# U1-9(D5) SIGNAL at Gerber(120000,120000)&lt;/span&gt;
&lt;span class="c1"&gt;# Region 20: 5 vertices within 2.54mm&lt;/span&gt;
&lt;span class="c1"&gt;#   (120000,116587) dist=0.87mm angle=-90°&lt;/span&gt;
&lt;span class="c1"&gt;#   (121413,128587) dist=2.21mm angle=81°&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Five vertices. Two distinct positions. A proper circular clearance cutout around a through-hole pin needs 8 to 16 vertices distributed at various angles to approximate the circle. This had vertices at just two angles: -90° and 81°. The polygon boundary was making a shallow notch past the pin, not encircling it.&lt;/p&gt;
&lt;p&gt;Even worse: the closest vertex was only 0.87mm from the pin center. The pad edge is at 0.762mm (half of the 1.524mm pad diameter). That left 0.108mm of clearance—about 4.3 mil—on the side where the boundary came closest. Manufacturing tolerance at PCBWay is typically 4-6 mil. The clearance was right at the edge, and on other sides of the pin, there was no clearance at all because the polygon boundary didn't go around.&lt;/p&gt;
&lt;p&gt;For comparison, U1 pin 29 (GND, which &lt;em&gt;should&lt;/em&gt; connect to the fill) had a vertex at exactly 0.00mm distance. The fill went right through it. Correct.&lt;/p&gt;
&lt;h3&gt;The Root Cause&lt;/h3&gt;
&lt;p&gt;pcb-rnd's Gerber exporter was generating incomplete clearance cutouts in the ground fill polygon around through-hole pins. Instead of tracing a complete circle around each pin (maintaining the specified 0.762mm clearance on all sides), it was generating partial notches that only cleared the pin on one or two sides. On the remaining sides, the ground fill copper made direct contact with the pin pad.&lt;/p&gt;
&lt;p&gt;This affected every through-hole pin inside the polygon's boundary: all of U1's pins, all of J1's pins, and most of the vias. The pattern now made sense. The ground fill polygon covered x = 0.3mm to 55.6mm—the left side of the board. U1 (at x = 30.48mm) was squarely inside. U2 (at x = 66.0mm) was outside. All 24 bus lines pass through U1's footprint. All 8 control lines connect only to one Z80 and route through traces, not through-hole pads, in the fill area.&lt;/p&gt;
&lt;p&gt;The reason the initial diagnostic showed control signals as "OK" while bus lines were "SHORT" was purely geometric: the control signal traces exited the fill area quickly and reached the Arduino pins via the top copper layer, while the bus lines had through-hole pads sitting directly in the fill.&lt;/p&gt;
&lt;p&gt;It's worth noting that this bug is specific to the combination of pcb-rnd's polygon fill, through-hole pins, and Gerber export. SMD pads weren't affected (no SMD components were inside the fill area). The clearance math in pcb-rnd's internal representation appeared correct, but the translation to Gerber region vertices lost fidelity, producing polygon outlines that didn't fully encircle the pins.&lt;/p&gt;
&lt;h3&gt;The Fix&lt;/h3&gt;
&lt;p&gt;I removed the ground fill polygon entirely.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# Find and remove the Polygon block in Layer 2 (bottom)&lt;/span&gt;
&lt;span class="c1"&gt;# Strategy: parse through the file, skip everything&lt;/span&gt;
&lt;span class="c1"&gt;# between 'Polygon("clearpoly")' and its closing ')'&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The board's GND connectivity doesn't depend on the fill. The autorouter (Freerouting) had already placed explicit copper traces connecting all GND pins. The fill was adding copper density and potentially improving signal integrity, but neither matters for a board running Z80s at 4 MHz. At these speeds, the electrical benefit of a ground plane is negligible, and the manufacturing risk (as we discovered) is real.&lt;/p&gt;
&lt;p&gt;I also cleaned up the erroneous flags while I was in there:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Removed &lt;code&gt;thermal(0X)&lt;/code&gt; from U1 pin 1 (A11 should not connect to any fill)&lt;/li&gt;
&lt;li&gt;Removed &lt;code&gt;connected&lt;/code&gt; from all +5V pins (J1-1, J1-36, U1-11, U1-24, U1-25, U2-11, U2-24, U2-25)&lt;/li&gt;
&lt;li&gt;Removed &lt;code&gt;thermal&lt;/code&gt;/&lt;code&gt;connected&lt;/code&gt; from all 11 vias (signal vias should get clearance, not connection)&lt;/li&gt;
&lt;li&gt;Left thermal flags only on GND pins: J1-18, J1-19, and U1-29&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The bottom copper Gerber went from 46KB (fill + traces) to 16KB (traces only). The region block count dropped from 26 to 5. Regenerated the Excellon drill file, rebuilt the production zip with BOM and centroid, and pushed everything to &lt;a href="https://github.com/ajokela/dual-z80"&gt;the repository&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Was This Always Broken?&lt;/h3&gt;
&lt;p&gt;A natural question: is this a flaw in the original RetroShield Z80 design, or something we introduced? The ground fill polygon, the &lt;code&gt;thermal(0X)&lt;/code&gt; flag on U1 pin 1, and all the &lt;code&gt;connected&lt;/code&gt; flags exist in &lt;a href="https://gitlab.com/8bitforce/retroshield-hw/-/tree/master/hardware/kz80"&gt;Erturk Kocalar's upstream design&lt;/a&gt; — identical to our initial commit. We didn't add any of them. So why does the original RetroShield work?&lt;/p&gt;
&lt;p&gt;To find out, I ran the original, unmodified &lt;code&gt;kz80.pcb&lt;/code&gt; through the same pcb-rnd Gerber exporter and performed the same vertex analysis on the output. The results were revealing:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pin&lt;/th&gt;
&lt;th&gt;Our Board (re-routed)&lt;/th&gt;
&lt;th&gt;Original Board (original traces)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;U1-9 (D5, signal)&lt;/td&gt;
&lt;td&gt;5 vertices, closest 0.87mm from center&lt;/td&gt;
&lt;td&gt;75 vertices, closest 1.14mm from center&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;U1-10 (D6, signal)&lt;/td&gt;
&lt;td&gt;similar&lt;/td&gt;
&lt;td&gt;169 vertices, closest 1.14mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;U1-29 (GND)&lt;/td&gt;
&lt;td&gt;vertex at 0.00mm (correct)&lt;/td&gt;
&lt;td&gt;vertices at 0.76mm (thermal spokes, correct)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The original board, exported through the &lt;em&gt;same&lt;/em&gt; pcb-rnd Gerber exporter, gets proper circular clearance cutouts with 75+ vertices distributed around each signal pin at a safe 1.14mm from center (0.38mm clearance from pad edge). Our re-routed board gets 5 vertices at only 0.87mm from center (0.11mm clearance — below manufacturing tolerance).&lt;/p&gt;
&lt;p&gt;The difference is the trace geometry. The original RetroShield's traces were routed with classic gEDA/pcb. When we added the second Z80, we stripped all traces and autorouted from scratch with Freerouting. The new trace layout changed how pcb-rnd's polygon fill algorithm tessellated the clearance boundaries. With different traces running through the fill area, the polygon's outline took different paths around the pins — and those paths didn't maintain adequate clearance.&lt;/p&gt;
&lt;p&gt;So the design flaw (wrong flags, a ground fill with tight clearance margins) was always latent in the original design. But it only manifested as physical shorts when the trace geometry changed. The original routing happened to produce geometry that pcb-rnd's exporter handled gracefully. Our autorouted traces didn't. It's the kind of bug that lies dormant until you touch something seemingly unrelated.&lt;/p&gt;
&lt;p&gt;This is why removing the polygon entirely was the right fix. It doesn't matter how the traces are routed if there's no fill to create clearance problems against. The GND connectivity is fully handled by explicit routed traces.&lt;/p&gt;
&lt;h3&gt;Verification&lt;/h3&gt;
&lt;p&gt;To confirm the fix, I verified the new Gerber output:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No layer polarity commands (no fill, no clearance needed)&lt;/li&gt;
&lt;li&gt;No large region blocks (no polygon fill)&lt;/li&gt;
&lt;li&gt;Only trace geometry and pad flashes remain on the bottom copper layer&lt;/li&gt;
&lt;li&gt;Bottom copper file size reduced by 65% (46KB → 16KB)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The new production files have been submitted to PCBWay for a second fabrication run. The fix is structural: without the fill polygon, there's nothing to short to. Every GND connection is an explicit routed trace, visible in the Gerber, and verifiable by inspection.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/dual-z80/kz80_top_hires.png" alt="Top view render of the corrected Rev C dual Z80 RetroShield PCB, showing clean routing without ground fill, both Z80 DIP-40 sockets, J1 bus header, and J2 control header" style="width: 100%; max-width: 800px; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); margin: 1.5em 0;"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The corrected Rev C board: 553 traces, 25 vias, no ground fill polygon. Clean explicit routing for all 48 nets.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;What I Learned&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Ground fills aren't free.&lt;/strong&gt; They improve signal integrity on high-speed boards, but on a 4 MHz Z80, the benefit is marginal. The cost is an additional failure mode: if the clearance generation is buggy, incomplete, or at the edge of manufacturing tolerance, the fill becomes a liability. For simple retro computing boards, explicit GND traces are more predictable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Autorouting can break things you didn't touch.&lt;/strong&gt; The ground fill polygon wasn't something we modified. But by re-routing the traces (which we had to do after adding the second Z80), we changed the geometry that the polygon fill algorithm used to compute clearances. A latent design flaw became an active one. When you re-route a board with copper fills, you need to re-verify the fills, not just the traces.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Test the bare PCB before populating it.&lt;/strong&gt; If I'd run a continuity test between the Z80 socket pads and GND before soldering anything, I'd have caught this immediately. Instead, I spent time debugging firmware, suspecting timing issues, and questioning my understanding of Z80 bus cycles. The problem was never software.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Gerber is the contract.&lt;/strong&gt; The PCB design tool's internal representation doesn't matter; only the Gerber output does. Even if pcb-rnd's polygon clearance looks correct on screen, the Gerber export is what the fab house uses. Verify the Gerber, not the design file. A Gerber viewer would have shown the incomplete clearances immediately.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Diagnostic sketches are invaluable.&lt;/strong&gt; Writing targeted Arduino sketches that tested individual pins, drove patterns, and reported results over serial turned a "nothing works" situation into a systematic narrowing process. The data bus loopback test (drive a byte, read it back, compare) is trivially simple and would have caught this on day one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI-assisted debugging works the same way AI-assisted design works.&lt;/strong&gt; I brought the domain knowledge (how Z80 bus signals work, what the control signal timing should look like, what "stuck LOW" means electrically) and the AI handled the tedious parts: writing diagnostic firmware, parsing Gerber files, analyzing polygon vertices, checking coordinate math. The same division of labor that made the design possible also made the debugging possible.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solder bridges are a red herring when the pattern is systematic.&lt;/strong&gt; Early on, I found and fixed a solder bridge between two adjacent pins. It didn't help. When two pins are bridged, you get two bad signals. When 24 pins are all shorted to the same rail, the cause is structural, not incidental. I should have recognized the pattern sooner and stopped looking at individual joints.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open-source hardware needs open-source verification.&lt;/strong&gt; The entire reason I could diagnose this was that every file in the chain—PCB source, Gerber output, Excellon drill files—was text-based, parseable, and inspectable. I wrote Python scripts to analyze the Gerber's region vertices and measure distances to pin centers. Try doing that with a proprietary board file. The text-based EDA workflow that made the design possible also made the debugging possible.&lt;/p&gt;
&lt;h3&gt;What's Next&lt;/h3&gt;
&lt;p&gt;The corrected boards are in fabrication. When they arrive, Part 3 will cover what this project was always about: bringing up the SMP kernel, watching two Z80 processors boot the same code, identify themselves, and divide work across a shared memory bus. The kernel is 52 bytes. The scheduler is in the Arduino. The demo sums an array split across both CPUs and measures the speedup.&lt;/p&gt;
&lt;p&gt;Both Z80s are confirmed alive. They just need a board that doesn't short their bus to ground.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The complete source—PCB files, Gerber production package, diagnostic sketches, SMP kernel, and wiring guide—is on &lt;a href="https://github.com/ajokela/dual-z80"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;</description><category>arduino</category><category>debugging</category><category>dual cpu</category><category>gerber</category><category>ground plane</category><category>hardware</category><category>pcb design</category><category>pcb fabrication</category><category>pcb-rnd</category><category>retro computing</category><category>retroshield</category><category>z80</category><guid>https://tinycomputers.io/posts/designing-a-dual-z80-retroshield-part-2.html</guid><pubDate>Mon, 06 Apr 2026 22:00:00 GMT</pubDate></item><item><title>Why Some Chips Last 40+ Years: Z80, 68k, 6502, and the Secret to Processor Longevity</title><link>https://tinycomputers.io/posts/why-some-chips-last-40-years.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;figure&gt;&lt;img src="https://tinycomputers.io/images/zilog-z80.jpg"&gt;&lt;/figure&gt; &lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/why-some-chips-last-40-years_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;22 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;There's a Zilog Z80 in a graphing calculator sitting in a high school classroom right now. The student using it was born around 2008. The Z80 was designed in 1976. That processor is older than the student's parents.&lt;/p&gt;
&lt;p&gt;This isn't a quirky footnote. It's a pattern. The Z80, the Motorola 68000, the MOS Technology 6502, the Intel 8051: these processors have been in continuous production and active deployment for forty years or more. The Z80 is closing in on fifty. Meanwhile, processors that were objectively superior by nearly every technical measure (the Zilog Z8000, the National Semiconductor 32016, the Motorola 88000, the Intel i960) are footnotes in Wikipedia articles that nobody reads.&lt;/p&gt;
&lt;p&gt;What determines whether a processor lives for decades or dies in five years? I've spent the last two years building &lt;a href="https://tinycomputers.io/posts/clean-room-z80-emulator.html"&gt;Z80 emulators&lt;/a&gt;, writing &lt;a href="https://tinycomputers.io/posts/building-language-compilers-for-the-z80.html"&gt;compilers for the Z80&lt;/a&gt;, running &lt;a href="https://tinycomputers.io/posts/cpm-on-physical-retroshield-z80.html"&gt;CP/M on physical RetroShield hardware&lt;/a&gt;, and exploring the &lt;a href="https://tinycomputers.io/posts/motorola-68000-processor-and-the-ti-89-graphing-calculator.html"&gt;Motorola 68000 through TI calculators&lt;/a&gt;. I've read William Barden's &lt;a href="https://tinycomputers.io/posts/the-z80-microcomputer-handbook-william-barden.html"&gt;1978 handbook&lt;/a&gt; that was still being reprinted in 1985, and Steve Ciarcia's &lt;a href="https://tinycomputers.io/posts/build-your-own-z80-computer-steve-ciarcia.html"&gt;build-your-own guide&lt;/a&gt; that assumed you'd wire up a computer from discrete chips. The deeper I've gone into this world, the more convinced I've become that processor longevity isn't really about the processor. It's about everything around it.&lt;/p&gt;
&lt;h3&gt;The Survivors&lt;/h3&gt;
&lt;p&gt;Four processors stand out for their extraordinary longevity. Each was introduced in the mid-to-late 1970s. Each is still manufactured or cloned today.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Zilog Z80&lt;/strong&gt; (1976) was designed by Federico Faggin and Masatoshi Shima, both of whom had worked on the Intel 4004 and 8080. The Z80 was explicitly designed as a better 8080, backward-compatible with the 8080's instruction set but adding indexed addressing, a second register bank, a built-in DRAM refresh counter, and a single 5V power supply (the 8080 needed three voltage rails). It became the heart of CP/M machines, arcade cabinets, and eventually TI graphing calculators. Zilog's CMOS variant, the Z84C00, was manufactured continuously until &lt;a href="https://baud.rs/IboIHD"&gt;April 2024&lt;/a&gt;, when Littelfuse (Zilog's current owner) finally announced end-of-life after 48 years. The eZ80, a backward-compatible enhanced variant, continues in production, and third-party clones remain available. The Z80 instruction set isn't going anywhere even if the original silicon is.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The MOS Technology 6502&lt;/strong&gt; (1975) was designed by Chuck Peddle and Bill Mensch after they left Motorola. At \$25 when competing processors cost \$150-\$300, the 6502 was a revolution in affordability. It powered the Apple II, the Commodore 64, the Atari 2600, and the NES. Bill Mensch's Western Design Center still manufactures the W65C02S and W65C816S today, fifty years after the original design.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/chip-longevity/hitachi-hd68000.jpg" alt="Hitachi HD68000, a second-sourced clone of the Motorola MC68000" style="width: 340px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: right; margin: 0 0 20px 20px;"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Motorola 68000&lt;/strong&gt; (1979) was the 32-bit processor that arrived a generation early. With a linear 24-bit address space and an orthogonal instruction set that programmers genuinely enjoyed using, it became the foundation for the original Macintosh, the Amiga, the Atari ST, the Sega Genesis, and Sun's first workstations. Its descendants (the 68020, 68030, 68040, ColdFire, and now NXP's modern variants) kept the architecture alive in embedded systems, automotive controllers, and &lt;a href="https://tinycomputers.io/posts/motorola-68000-processor-and-the-ti-89-graphing-calculator.html"&gt;Texas Instruments calculators&lt;/a&gt; well into the 2020s.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/chip-longevity/intel-p8051.jpg" alt="Intel P8051 microcontroller in DIP-40 package" style="width: 340px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: left; margin: 0 20px 20px 0;"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Intel 8051&lt;/strong&gt; (1980) is perhaps the most quietly ubiquitous processor ever made. Designed as a microcontroller (a processor with RAM, ROM, timers, and I/O ports integrated on a single chip), the 8051 found its way into everything from washing machines to automotive engine controllers to industrial PLCs. Over two dozen companies have manufactured 8051 variants. If you've used an appliance, driven a car, or walked through a building with an elevator in the last forty years, you've interacted with an 8051 derivative.&lt;/p&gt;
&lt;p&gt;The 8051 is also a case study in &lt;a href="https://tinycomputers.io/posts/jevons-paradox.html"&gt;Jevons Paradox&lt;/a&gt; applied to silicon. As more manufacturers licensed and produced the 8051, unit costs fell. As unit costs fell, engineers designed it into applications that would never have justified a microcontroller at the original price: a toaster, a thermostat, a toy. Each new application expanded the market, which attracted more manufacturers, which drove costs lower still. The cycle fed itself for decades. Technically superior alternatives existed at every point along this curve, but they couldn't compete with an architecture whose ecosystem was compounding while their price-per-unit was still on the wrong side of the volume curve.&lt;/p&gt;
&lt;h3&gt;The Fallen&lt;/h3&gt;
&lt;p&gt;For every processor that lasted decades, dozens vanished. Some of these were technically impressive, arguably more capable than the survivors.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;Zilog Z8000&lt;/strong&gt; (1979), designed as the Z80's successor, offered a 16-bit architecture with segmented memory addressing. It was more powerful than the Z80 in every measurable way. It lasted roughly five years in the market before fading into obscurity. The segmented memory model (the same curse that plagued Intel's 8086/286) made programming painful. And critically, it wasn't backward-compatible with the Z80. Every Z80 program, every CP/M application, every line of existing code was useless on the Z8000. Zilog was asking customers to abandon their entire software investment.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;Motorola 88000&lt;/strong&gt; (1988) was Motorola's clean-sheet RISC design, intended to eventually replace the 68k family. It was technically excellent: pipelined, superscalar-capable, and well-designed. Motorola couldn't sell it. Customers had millions of lines of 68k code, working products, trained engineers, and proven toolchains. The 88000 offered better performance but required abandoning everything. Motorola eventually surrendered and joined IBM and Apple to create the PowerPC, which at least had the marketing muscle of three companies behind it.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;National Semiconductor 32016&lt;/strong&gt; (1982) was a full 32-bit processor at a time when the PC world was still on 16-bit. It was used in the Acorn Cambridge Workstation and a few other systems. It had bugs. The early silicon had errata that made reliable system design difficult. By the time National got the bugs out, the market had moved on.&lt;/p&gt;
&lt;p&gt;The pattern is consistent: technical superiority alone doesn't determine survival.&lt;/p&gt;
&lt;h3&gt;Five Factors That Determine Processor Longevity&lt;/h3&gt;
&lt;p&gt;After spending years in this world, I've identified five factors that separate the survivors from the fallen. They're listed roughly in order of importance, which is not the order most engineers would expect.&lt;/p&gt;
&lt;h4&gt;1. Second-Sourcing and Licensing&lt;/h4&gt;
&lt;p&gt;This is the single most important factor, and it's the one that engineers consistently underrate because it's a business decision, not a technical one.&lt;/p&gt;
&lt;p&gt;The Z80 was second-sourced by Mostek, SGS-Thomson, Sharp, NEC, Toshiba, Samsung, and others. When &lt;a href="https://www.littelfuse.com/"&gt;Littelfuse&lt;/a&gt;, the current owner of Zilog, finally discontinued the standalone Z84C00 in 2024, the instruction set didn't die, because it was never dependent on a single manufacturer. This is exactly what second-sourcing was designed to protect against. It mattered enormously to design engineers in the 1980s and 1990s, because committing a product design to a single-source processor was career-threatening. If your sole supplier had a fab fire, or went out of business, or simply decided to discontinue the chip, your product was dead.&lt;/p&gt;
&lt;p&gt;The 6502 was licensed to multiple manufacturers: Rockwell, Synertek, GTE, and later CMD and the Western Design Center. The 8051 took this to its logical extreme: Intel actively encouraged licensing, and the architecture was eventually manufactured by Atmel, Philips/NXP, Silicon Labs, Dallas/Maxim, Infineon, and dozens more. The 8051 became less a product and more a standard, an instruction set architecture that any competent semiconductor company could implement. It was, in hindsight, a preview of the model that ARM and RISC-V would later formalize: sell the design, not the chip, and let the ecosystem do the rest.&lt;/p&gt;
&lt;p&gt;The 68000 family was produced by Motorola, Hitachi, Signetics, Mostek, and Toshiba. Later, the ColdFire and subsequent architectures maintained enough compatibility to keep the ecosystem alive under Freescale and then NXP.&lt;/p&gt;
&lt;p&gt;The x86 architecture tells the same story at a larger scale. IBM refused to use Intel's 8088 in the original PC without a second source. That requirement forced Intel to license the design to AMD, a decision Intel spent the next four decades regretting and litigating. But the resulting duopoly is a major reason x86 survived the RISC revolution of the 1990s. When Sun, SGI, and DEC were pushing SPARC, MIPS, and Alpha, customers considering a switch to RISC had to weigh superior performance against the uncomfortable fact that each RISC architecture had exactly one supplier. x86 had two. That mattered more than clock speeds.&lt;/p&gt;
&lt;p&gt;Contrast all of this with the Z8000, which was essentially Zilog-only. Or the 88000, which was Motorola-only. Single-source processors carry existential risk for every product that uses them. Purchasing managers know this even when engineers don't.&lt;/p&gt;
&lt;h4&gt;2. Ecosystem and Toolchain Maturity&lt;/h4&gt;
&lt;p&gt;A processor without a mature toolchain is a science project. A processor with assemblers, compilers, debuggers, reference designs, application notes, textbooks, and a community of experienced engineers is an ecosystem.&lt;/p&gt;
&lt;p&gt;The Z80 ecosystem by the mid-1980s was staggering. There were books (&lt;a href="https://baud.rs/EZ3Bwg"&gt;Rodnay Zaks' &lt;em&gt;Programming the Z80&lt;/em&gt;&lt;/a&gt;, Barden's &lt;a href="https://baud.rs/5brWaW"&gt;handbook&lt;/a&gt;, Ciarcia's &lt;a href="https://baud.rs/kiLcPY"&gt;build guide&lt;/a&gt;, Coffron's &lt;a href="https://baud.rs/3hw1CF"&gt;applications manual&lt;/a&gt;) available at any technical bookstore. There were assemblers, C compilers, BASIC interpreters, and Forth systems. There were thousands of CP/M applications. There were magazines publishing Z80 projects monthly. There were university courses teaching Z80 assembly. Every year, this ecosystem grew, and every year, the cost of switching to a different processor increased.&lt;/p&gt;
&lt;p&gt;The 6502 had a similar ecosystem, driven heavily by the Apple II and Commodore 64 communities. The 8051 accumulated the largest ecosystem of any microcontroller family, with Keil (now ARM), IAR, SDCC, and many other toolchains providing development environments across every host platform.&lt;/p&gt;
&lt;p&gt;When I wrote about &lt;a href="https://tinycomputers.io/posts/how-we-learned-hardware-in-1983.html"&gt;how we learned hardware in 1983&lt;/a&gt;, I was documenting a snapshot of this ecosystem at its peak. Those books, those reference designs, those shared conventions: they weren't just educational resources. They were infrastructure. And infrastructure, once built, resists replacement.&lt;/p&gt;
&lt;h4&gt;3. ISA Simplicity and Predictability&lt;/h4&gt;
&lt;p&gt;There's a counterintuitive truth about instruction set architecture: the "best" ISA often isn't the one that survives. The one that survives is the one that's simple enough to implement cheaply, predictable enough to verify thoroughly, and small enough to teach in a semester.&lt;/p&gt;
&lt;p&gt;The Z80's instruction set is large by 8-bit standards, with 158 base instructions and variants pushing toward 700 when you count all the addressing modes. But the fundamental execution model is simple: fetch an instruction, decode it, execute it. No pipeline. No branch prediction. No speculative execution. No out-of-order dispatch. The behavior is deterministic. If you clock the Z80 at 4 MHz, you can calculate exactly how many T-states each instruction takes and predict your program's execution time down to the microsecond.&lt;/p&gt;
&lt;p&gt;This determinism is extraordinarily valuable in embedded systems. When you're designing an engine controller or a medical device, you need to know (not estimate, &lt;em&gt;know&lt;/em&gt;) that your interrupt handler will complete within a specific time window. Pipelined processors with branch prediction make this analysis much harder. Simple processors make it trivial.&lt;/p&gt;
&lt;p&gt;The 6502 takes this even further. With only 56 instructions and 13 addressing modes, the entire ISA fits on a single reference card. You can hold the complete instruction set in your head. This isn't a limitation; it's a feature. Engineers who can reason about every instruction their processor executes build more reliable systems than engineers who rely on abstractions they don't fully understand.&lt;/p&gt;
&lt;p&gt;The 8051 instruction set is similarly compact: 111 instructions, most executing in one or two machine cycles. The architecture includes bit-addressable memory, a feature that seems quirky until you're writing firmware for a device with dozens of individual control signals, at which point it becomes indispensable.&lt;/p&gt;
&lt;h4&gt;4. Power, Size, and Cost&lt;/h4&gt;
&lt;p&gt;The survivors share a common economic profile: they're cheap to manufacture, cheap to buy, and cheap to power.&lt;/p&gt;
&lt;p&gt;A Z84C00 in CMOS draws microwatts in standby. A W65C02S runs on a coin cell battery for years. An 8051 derivative can be manufactured on mature process nodes that have been paid for decades ago, with die sizes so small that the packaging costs more than the silicon. When your processor costs \$0.50 in volume and runs on the leakage current of a lithium cell, the engineering case for replacing it with something faster but more expensive becomes very hard to make.&lt;/p&gt;
&lt;p&gt;This is where processor longevity intersects with the economics I've written about in the &lt;a href="https://tinycomputers.io/posts/jevons-paradox.html"&gt;Jevons Paradox series&lt;/a&gt;. The relevant cost isn't just the chip; it's the total cost of the design: the processor, the toolchain, the engineering time, the qualification testing, the regulatory certification, and the opportunity cost of a redesign. A \$0.50 Z80 clone in a proven design with ten years of field data is almost impossible to displace, even if a \$0.30 ARM Cortex-M0 is technically superior, because the redesign and requalification costs dwarf the per-unit savings.&lt;/p&gt;
&lt;h4&gt;5. Inertia and Institutional Knowledge&lt;/h4&gt;
&lt;p&gt;The final factor is the hardest to quantify and the most powerful: institutional inertia.&lt;/p&gt;
&lt;p&gt;Somewhere in Germany, there's a factory running a production line controlled by Z80-based PLCs installed in 1988. The line produces automotive components. It runs 24/7. It works. The engineer who designed the control system retired fifteen years ago. The firmware was written in Z80 assembly and documented in a binder that lives in a filing cabinet near the line.&lt;/p&gt;
&lt;p&gt;Replacing this system would require: reverse-engineering the existing firmware (the original source code may or may not still exist), designing a new control system, writing new firmware, testing it against every production scenario the old system handles, qualifying the new system for automotive safety standards, scheduling downtime for installation, and training operators on the new system. The cost runs into hundreds of thousands of dollars. The risk is non-trivial; any bug could halt production.&lt;/p&gt;
&lt;p&gt;So they order more Z80s. And the Z80 stays in production for another year.&lt;/p&gt;
&lt;p&gt;Multiply this scenario by thousands of factories, millions of installed devices, and billions of lines of proven firmware, and you begin to understand why some processors simply cannot die. The cost of replacing them exceeds the cost of maintaining them, indefinitely.&lt;/p&gt;
&lt;p&gt;This is also why the &lt;a href="https://tinycomputers.io/posts/exploring-ti-84%2B.html"&gt;TI-84+ still uses a Z80&lt;/a&gt;. Texas Instruments has decades of TI-BASIC software, decades of teacher training materials, decades of standardized test approvals, and a user base that expects backward compatibility with programs written in 2004. The Z80 isn't the best processor for a modern calculator. But replacing it would require replacing &lt;em&gt;everything else&lt;/em&gt;, and "everything else" is where the real value lives.&lt;/p&gt;
&lt;h3&gt;The Newcomen Pattern&lt;/h3&gt;
&lt;p&gt;There's a historical analogy I keep returning to. Thomas Newcomen built his atmospheric steam engine in 1712. It was inefficient, converting roughly 1% of the heat energy in coal into useful work. James Watt's improved design, introduced in the 1760s, was dramatically better: separate condenser, double-acting cylinder, and eventually five times the thermal efficiency. By any rational engineering measure, the Newcomen engine should have vanished overnight.&lt;/p&gt;
&lt;p&gt;It didn't. Newcomen engines continued to be built and operated for decades after Watt's design was available. In some mining operations, they remained in service into the 19th century. The reasons were the same ones that keep Z80s in factories today: the existing engines worked, the operators knew how to maintain them, the replacement cost was high, and the performance of the old engine was &lt;em&gt;adequate&lt;/em&gt; for the task.&lt;/p&gt;
&lt;p&gt;"Adequate for the task" is the phrase that explains processor longevity better than any technical specification. The Z80 is adequate for a graphing calculator. The 6502 is adequate for a simple embedded controller. The 8051 is adequate for a washing machine. And "adequate" plus "proven" plus "cheap" plus "available from multiple sources" is a combination that "superior but new and unfamiliar" almost never beats.&lt;/p&gt;
&lt;h3&gt;The Numbers Tell the Story&lt;/h3&gt;
&lt;p&gt;It's worth pausing to appreciate the sheer scale of the survivors' deployment.&lt;/p&gt;
&lt;p&gt;The 8051 family has been manufactured in quantities estimated at over 10 billion units. That's not a typo. Ten billion. More 8051 derivatives have been produced than any other processor architecture in history, including x86. They're in your car; a modern automobile contains dozens of microcontrollers, many of them 8051 variants, handling everything from window controls to tire pressure monitoring. They're in your thermostat, your microwave, your garage door opener.&lt;/p&gt;
&lt;p&gt;The Z80 and its clones have shipped in quantities that are harder to pin down precisely, but conservative estimates exceed a billion units across all manufacturers and derivatives. The 6502 family, counting all variants from the original through the 65C816 that powered the Apple IIGS and the Super Nintendo, is in a similar range.&lt;/p&gt;
&lt;p&gt;The 68000 family took a different path: fewer total units but higher-value applications. Where the 8051 went wide and cheap, the 68k went deep and capable. It dominated the workstation market before RISC architectures displaced it, then settled into a long career in automotive and industrial control. NXP's ColdFire and subsequent QorIQ Layerscape processors carry DNA that traces back to the original 68000. The architecture didn't die; it evolved.&lt;/p&gt;
&lt;p&gt;What's remarkable about these numbers is that they &lt;em&gt;continue to grow&lt;/em&gt;. These aren't static installed bases slowly decaying as old equipment is retired. New products are still being designed with 8051 cores. New Z80-compatible processors are still being fabricated; even after Littelfuse discontinued the original Z84C00 in 2024, third-party clones and the eZ80 keep the instruction set alive. When I built a &lt;a href="https://tinycomputers.io/posts/designing-a-dual-z80-retroshield-part-1.html"&gt;dual Z80 RetroShield&lt;/a&gt;, I ordered Z84C0020PEC chips that were still in stock from the final production runs. A 1976 design, manufactured nearly half a century later. And the fact that Zilog's discontinuation made international headlines tells you everything about how deeply embedded these chips remain. You don't mourn a processor nobody uses.&lt;/p&gt;
&lt;h3&gt;What This Means for Modern Processors&lt;/h3&gt;
&lt;p&gt;The ARM Cortex-M0, introduced in 2009, is arguably the first modern processor that has a plausible shot at matching the longevity of the 8-bit survivors. It's licensable (like the 8051), simple (like the 6502), power-efficient (like the Z84C00), and backed by an ecosystem that's growing rapidly. ARM's licensing model (selling the design, not the chip) mirrors the model that made the 8051 ubiquitous.&lt;/p&gt;
&lt;p&gt;RISC-V, as an open ISA, goes even further. No licensing fees, no single company that can discontinue the architecture, no vendor lock-in. I've &lt;a href="https://tinycomputers.io/posts/milk-v-mars-review.html"&gt;reviewed RISC-V boards&lt;/a&gt; and watched the ecosystem grow. If any modern ISA is positioned to last fifty years, it's RISC-V, not because it's the best architecture, but because it's the hardest to kill.&lt;/p&gt;
&lt;p&gt;But here's the uncomfortable truth for anyone designing a new processor architecture: the window for establishing a forty-year processor is probably closed. The Z80, 6502, 68000, and 8051 all emerged during a period when the microprocessor market was being established. There were no entrenched incumbents. Every design win was greenfield. Every new application (calculators, arcade cabinets, industrial controllers, medical devices) was being designed for the first time with microprocessors.&lt;/p&gt;
&lt;p&gt;That era is over. Every new design now competes against an installed base. Every new ISA competes against ARM's ecosystem. The switching costs that keep forty-year-old processors alive are the same switching costs that prevent new architectures from gaining traction. The moat works in both directions.&lt;/p&gt;
&lt;h3&gt;The Lesson&lt;/h3&gt;
&lt;p&gt;The processors that last aren't the ones that push the performance envelope. They're the ones that solve a problem well enough, cheaply enough, reliably enough, and from enough sources that replacing them is never worth the trouble. Technical excellence is necessary but not sufficient. What matters more is the web of dependencies (the toolchains, the trained engineers, the certified designs, the proven firmware, the institutional knowledge) that accumulates around a processor over decades.&lt;/p&gt;
&lt;p&gt;The Z80 will outlive many of the engineers reading this, not because it's a great processor, but because it's woven into the fabric of systems that nobody has a compelling reason to redesign. The 8051 will outlive the Z80, because it's woven into even more systems. And somewhere in a high school classroom, a student is pressing buttons on a &lt;a href="https://tinycomputers.io/posts/exploring-ti-84%2B.html"&gt;TI-84+&lt;/a&gt; that runs on a fifty-year-old instruction set, completely unaware that the chip executing their quadratic formula has been doing this job since before their grandparents started dating.&lt;/p&gt;
&lt;p&gt;That's longevity. Not the kind you engineer. The kind that happens when everything around the chip conspires to keep it in place.&lt;/p&gt;
&lt;div style="margin-top: 3em; padding-top: 1em; border-top: 1px solid #ccc; font-size: 0.85em; color: #666;"&gt;
&lt;strong&gt;Image credits:&lt;/strong&gt; Hitachi HD68000 and Intel P8051 photographs by Konstantin Lanzet, via &lt;a href="https://commons.wikimedia.org/wiki/File:KL_Hitachi_HD68000.jpg"&gt;Wikimedia Commons&lt;/a&gt;. Licensed under GFDL and CC BY-SA 3.0 respectively.
&lt;/div&gt;</description><category>6502</category><category>8051</category><category>68000</category><category>embedded systems</category><category>isa</category><category>microprocessors</category><category>mos technology</category><category>motorola</category><category>processor architecture</category><category>retrocomputing</category><category>second-sourcing</category><category>z80</category><category>zilog</category><guid>https://tinycomputers.io/posts/why-some-chips-last-40-years.html</guid><pubDate>Sun, 08 Mar 2026 16:00:00 GMT</pubDate></item><item><title>Designing a Dual Z80 RetroShield: Two CPUs, One Bus, Zero GUI (Part 1)</title><link>https://tinycomputers.io/posts/designing-a-dual-z80-retroshield-part-1.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/designing-a-dual-z80-retroshield-part-1_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;19 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;&lt;img src="https://tinycomputers.io/images/dual-z80/zilog-scc-dip40.jpeg" alt="A Zilog Z0853006PSC SCC chip in a DIP-40 package, marked with the Zilog logo and a 1981 copyright date" style="float: right; max-width: 300px; margin: 0 0 1em 1.5em; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);"&gt;&lt;/p&gt;
&lt;p&gt;The RetroShield Z80 by Erturk Kocalar at &lt;a href="https://baud.rs/87wbBL"&gt;8bitforce.com&lt;/a&gt; is one of my favorite pieces of hardware. A real Zilog Z80 CPU on a shield that plugs into an Arduino Mega. The Arduino emulates memory and I/O while the Z80 executes real instructions on real silicon. I've used it to &lt;a href="https://tinycomputers.io/posts/cpm-on-physical-retroshield-z80.html"&gt;boot CP/M&lt;/a&gt;, &lt;a href="https://tinycomputers.io/posts/zork-on-retroshield-z80-arduino-giga.html"&gt;play Zork over WiFi&lt;/a&gt;, &lt;a href="https://tinycomputers.io/posts/cpm-on-arduino-giga-r1-wifi.html"&gt;port it to the Arduino Giga R1&lt;/a&gt;, and even &lt;a href="https://tinycomputers.io/posts/fiverr-pcb-design-arduino-giga-shield.html"&gt;commission a custom level-converter shield&lt;/a&gt; to bridge the voltage gap.&lt;/p&gt;
&lt;p&gt;But a single Z80 is, well, a single Z80. Real multi-processor Z80 systems existed in the 1980s. Machines like the &lt;a href="https://baud.rs/tTpLxt"&gt;Cromemco System Three&lt;/a&gt; and some S-100 configurations ran multiple Z80s on a shared bus, with bus arbitration mediating access. The question that kept nagging at me: could I fit a second Z80 onto the RetroShield?&lt;/p&gt;
&lt;p&gt;I should be honest about something: PCB design is one of my least knowledgeable areas of computing. I'm comfortable with firmware, with compilers, with operating systems, but the physical layer, the world of copper traces and drill files and design rule checks, is territory I've mostly avoided. I can read a schematic, but I've never designed a board from scratch. What I wanted to find out was whether modern AI tools could bridge that gap, whether I could use AI to help me understand, alter, and extend &lt;a href="https://baud.rs/87wbBL"&gt;Erturk Kocalar's&lt;/a&gt; existing RetroShield design into something new without becoming a PCB design expert first.&lt;/p&gt;
&lt;p&gt;This is part one of a two-part series. This piece covers the design: architecture decisions, schematic work, PCB layout, autorouting, and Gerber generation. Part two will cover the physical boards arriving from the fab, assembly, bring-up, and the firmware that makes two Z80s cooperate.&lt;/p&gt;
&lt;p&gt;One more thing worth mentioning up front: every step of this design was done without a GUI. That was intentional. I wanted to see how far I could get with just a terminal, command-line EDA tools, AI assistance, and Python scripts that modify PCB files directly. Partly because I think text-based workflows compose better with AI; it's much easier for an AI to generate a Python script that manipulates a text-based PCB file than to drive a graphical EDA tool. And partly because I wanted the entire process to be reproducible and scriptable, not trapped in a series of mouse clicks I'd never remember.&lt;/p&gt;
&lt;h3&gt;The Original Design&lt;/h3&gt;
&lt;p&gt;The &lt;a href="https://gitlab.com/8bitforce/retroshield-hw/-/tree/master/hardware/kz80?ref_type=heads"&gt;stock RetroShield Z80&lt;/a&gt; is a clean, simple board. A 55.88mm × 53.34mm two-layer PCB carrying:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;U1&lt;/strong&gt;: A &lt;a href="https://baud.rs/FUCwFg"&gt;Z80 CPU&lt;/a&gt; in a DIP-40 package&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;J1&lt;/strong&gt;: A 2×18 pin header (36 pins) that plugs into &lt;a href="https://baud.rs/CWPoOM"&gt;Arduino Mega 2560&lt;/a&gt; pins 22–53&lt;/li&gt;
&lt;li&gt;A handful of passives: decoupling caps (C1, C2), a clock cap (C3), a clock series resistor (R1), an LED current-limiting resistor (R3), and a bus activity LED&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The J1 header carries everything the Z80 needs: 16 address lines (A0–A15), 8 data lines (D0–D7), and control signals (CLK, RESET, INT, NMI, MREQ, IORQ, RD, WR). The Arduino drives the clock, provides the data when the Z80 reads, captures the data when the Z80 writes, and emulates whatever memory and I/O map you define in firmware. It's elegant in its simplicity; the Z80 thinks it's talking to a real computer, and in a sense, it is.&lt;/p&gt;
&lt;p&gt;The schematic and PCB files use the gEDA format, text-based files that are human-readable and, crucially, scriptable. The schematic (&lt;code&gt;.sch&lt;/code&gt;) defines the logical connections. The PCB (&lt;code&gt;.pcb&lt;/code&gt;) defines the physical layout: component footprints, copper traces, vias, and board outline. Both are just text. This matters a lot for what comes next.&lt;/p&gt;
&lt;h3&gt;Why Two Z80s?&lt;/h3&gt;
&lt;p&gt;The honest answer is that I wanted to see if it could be done. But there are genuinely interesting things you can do with two processors sharing a bus:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Asymmetric multiprocessing.&lt;/strong&gt; One Z80 runs CP/M as the primary CPU. The second handles I/O (serial communication, disk access, network operations), freeing the primary CPU from waiting on slow peripherals. This mirrors how some S-100 systems used coprocessor boards.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cooperative multitasking.&lt;/strong&gt; Both CPUs execute independent programs, taking turns on the shared bus. The Arduino arbitrates access using the Z80's built-in BUSRQ/BUSACK mechanism, a hardware handshake designed exactly for this purpose. One CPU gets the bus, executes for a while, then yields so the other can run.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Debugging and instrumentation.&lt;/strong&gt; The second CPU can monitor the first. Watch the address bus to trace execution. Compare outputs. Run the same code on both CPUs and verify they produce identical results, which is useful for testing Z80 clones or FPGA implementations against real silicon.&lt;/p&gt;
&lt;p&gt;The Z80 was designed for multiprocessor operation. As Rodnay Zaks details in &lt;a href="https://baud.rs/IvCPVA"&gt;&lt;em&gt;Programming the Z80&lt;/em&gt;&lt;/a&gt;, it has dedicated bus request (BUSRQ) and bus acknowledge (BUSAK) pins specifically for multi-master bus sharing. Steve Ciarcia's &lt;a href="https://baud.rs/eLG5hK"&gt;&lt;em&gt;Build Your Own Z80 Computer&lt;/em&gt;&lt;/a&gt; covers the hardware side of these signals in practical detail. Most hobbyist projects never use them. This one does.&lt;/p&gt;
&lt;h3&gt;Architecture: Shared Bus with Independent Control&lt;/h3&gt;
&lt;p&gt;The first design I considered (and quickly rejected) gave each Z80 its own independent header. Two 36-pin headers, two complete sets of address, data, and control lines. This would have worked electrically, but it was wrong for several reasons. It would have required either two Arduino Megas or consumed all the I/O on one Mega with nothing left for bus arbitration. The board would have been enormous. And it wouldn't have reflected how real multi-processor Z80 systems actually worked.&lt;/p&gt;
&lt;p&gt;The right approach is a shared bus. Both Z80s connect to the same address and data lines through J1. They take turns driving the bus, just like in a real S-100 system. What each CPU needs independently is its own set of control signals: its own clock, its own reset, its own interrupt lines, and its own bus request/acknowledge pair.&lt;/p&gt;
&lt;p&gt;I checked the Arduino Mega's pin budget. J1 uses pins 22–53 (32 I/O pins). The Mega still has pins 2–21 (20 pins) plus analog pins A0–A15 (16 more, usable as digital I/O), leaving 36 pins sitting idle. A second CPU's control signals only need about 10 pins. There was plenty of room.&lt;/p&gt;
&lt;p&gt;The solution: a small supplementary 2×6 header (J2, 12 pins) carrying CPU2's independent control signals to the Arduino's remaining pins:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;Pin 1:  +5V         Pin 2:  GND
Pin 3:  CLK_2       Pin 4:  RESET_2
Pin 5:  INT_2       Pin 6:  NMI_2
Pin 7:  MREQ_2      Pin 8:  IORQ_2
Pin 9:  RD_2        Pin 10: WR_2
Pin 11: BUSRQ_2     Pin 12: BUSAK_2
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;BUSRQ and BUSAK are the key pins. The Arduino firmware pulls BUSRQ low on whichever CPU should yield the bus. That CPU finishes its current machine cycle, tristates its outputs, and asserts BUSAK to signal it's off the bus. The other CPU can then drive the bus freely. It's the same mechanism Zilog designed in 1976; I'm just finally using it.&lt;/p&gt;
&lt;h3&gt;Building the Schematic, Without a Schematic Editor&lt;/h3&gt;
&lt;p&gt;The original project used classic gEDA tools (gschem, pcb), which are no longer packaged for Ubuntu 24.04. The modern replacement is lepton-eda, a maintained fork that reads the same file formats. But since the whole point was to avoid a GUI, even lepton-schematic's graphical mode was off the table.&lt;/p&gt;
&lt;p&gt;This is where AI earned its keep. I don't have the gEDA file format memorized; I've never needed to. But AI can work through the format specification and generate correct output. I described what I wanted (a second Z80 sharing the existing bus, with independent control signals on a new header), and the AI helped me produce the schematic files, the symbol definitions, and eventually the PCB modifications. I still had to understand the architecture and make the design decisions, but the AI handled the translation from intent to file format.&lt;/p&gt;
&lt;p&gt;gEDA schematic files are text. A component placement looks like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;C 44300 47700 1 0 0 z80-1.sym
{
T 44400 59000 5 10 1 1 0 0 1
refdes=U2
}
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That's a Z80 symbol placed at coordinates (44300, 47700), with reference designator U2. Net connections are similarly textual. &lt;code&gt;N&lt;/code&gt; entries define wire segments, &lt;code&gt;U&lt;/code&gt; entries define bus rippers. You can write an entire schematic in a text editor if you understand the coordinate system.&lt;/p&gt;
&lt;p&gt;I created a new schematic page, &lt;code&gt;kz80_cpu2.sch&lt;/code&gt;, for the second CPU. In gEDA's multi-page scheme, nets with the same name on different pages are automatically connected. So CPU2's address pins connect to nets named A0, A1, ..., A15 (the same net names used on page 1), and the netlister merges them into shared nets. The shared bus happens at the netlist level without any explicit cross-page wiring.&lt;/p&gt;
&lt;p&gt;The one component that didn't exist yet was the 2×6 control header. I wrote a new gEDA symbol file (&lt;code&gt;ctrlhdr2x6-1.sym&lt;/code&gt;) from scratch, a rectangular body with 12 pins, labeled with the control signal names, specifying the HEADER12_1 footprint. It's about 30 lines of text, all hand-written.&lt;/p&gt;
&lt;p&gt;CPU2's schematic connections break down cleanly:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shared with CPU1&lt;/strong&gt; (same net names, auto-merged): A0–A15, D0–D7, +5V, GND&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Independent to CPU2&lt;/strong&gt; (new nets with &lt;code&gt;_2&lt;/code&gt; suffix): CLK_2, RESET_2, INT_2, NMI_2, MREQ_2, IORQ_2, RD_2, WR_2, BUSRQ_2, BUSAK_2&lt;/p&gt;
&lt;p&gt;The total net count went from 37 to 48, only 11 new nets for an entirely new processor. That's the elegance of the shared-bus approach.&lt;/p&gt;
&lt;h3&gt;Modifying the PCB With Python&lt;/h3&gt;
&lt;p&gt;Here's where the CLI-only constraint got interesting. The normal workflow would be: run &lt;code&gt;lepton-sch2pcb&lt;/code&gt; to update the PCB with new components from the schematic, then open the PCB in a graphical editor to place and route them. But &lt;code&gt;lepton-sch2pcb&lt;/code&gt; had trouble finding footprints in pcb-rnd's library paths, and I didn't have a graphical editor anyway.&lt;/p&gt;
&lt;p&gt;So I had AI write a Python script (&lt;code&gt;add_cpu2_shared.py&lt;/code&gt;) to modify the PCB file directly. The pcb-rnd file format is text-based, with clearly delimited blocks for each component (Element), each copper trace (Line), each via (Via), and the netlist (NetList). The script:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Widened the board&lt;/strong&gt; from 55.88mm to 86.36mm, an extra 30.48mm to accommodate the second Z80 and control header, placed on the right half of the board.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Inserted five new Element blocks&lt;/strong&gt;: U2 (Z80, DIP-40), J2 (2×6 header), C4 and C5 (decoupling and clock caps), and R2 (clock series resistor). Each Element block is essentially a footprint definition: pin positions, pad dimensions, drill sizes, silkscreen outlines. I copied the dimensional parameters from the existing components to maintain consistency.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Updated the netlist&lt;/strong&gt; in two ways. For shared nets (A0–A15, D0–D7, +5V, GND), the script found each existing net block and appended &lt;code&gt;Connect("U2-xx")&lt;/code&gt; entries. For CPU2's independent control signals, it created 11 entirely new net blocks. The +5V net picked up four new connections: U2's VCC pin, U2's WAIT pin (tied high, since WAIT is active low, so high means "not waiting"), C4, and J2.&lt;/p&gt;
&lt;p&gt;The result was a valid PCB file with all components placed and all nets defined, but no copper traces connecting anything.&lt;/p&gt;
&lt;h3&gt;Autorouting: Let the Machine Do the Tedious Part&lt;/h3&gt;
&lt;p&gt;With components placed and nets defined, the board needed routing: actual copper traces connecting all those pins. Doing this by hand over SSH would have been masochistic. This is exactly what autorouters exist for.&lt;/p&gt;
&lt;p&gt;The workflow: export the PCB to Specctra DSN format (an industry-standard interchange format for autorouters), run &lt;a href="https://baud.rs/bdZw62"&gt;Freerouting&lt;/a&gt;, then import the results back.&lt;/p&gt;
&lt;h4&gt;First Attempt (Failed)&lt;/h4&gt;
&lt;p&gt;The first attempt exported the PCB with the original CPU1 traces still in place, hoping Freerouting would preserve them and only route the new nets. Instead, Freerouting spent 50+ seconds per pass trying to work around traces it couldn't associate with its own net encoding. After 48 passes and 40 minutes, it was still failing to route several nets.&lt;/p&gt;
&lt;h4&gt;Second Attempt (Clean Slate)&lt;/h4&gt;
&lt;p&gt;Another AI-generated Python script (&lt;code&gt;strip_traces.py&lt;/code&gt;) removed all existing copper traces from the PCB file. This was a careful operation. The script had to remove &lt;code&gt;Line[...]&lt;/code&gt; entries inside Layer blocks (copper traces) while preserving &lt;code&gt;ElementLine[...]&lt;/code&gt; entries (component silkscreen outlines that look syntactically similar).&lt;/p&gt;
&lt;p&gt;With a clean board, Freerouting ran in headless mode:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;java&lt;span class="w"&gt; &lt;/span&gt;-jar&lt;span class="w"&gt; &lt;/span&gt;/tmp/freerouting.jar&lt;span class="w"&gt; &lt;/span&gt;-de&lt;span class="w"&gt; &lt;/span&gt;kz80.dsn&lt;span class="w"&gt; &lt;/span&gt;-do&lt;span class="w"&gt; &lt;/span&gt;kz80.ses&lt;span class="w"&gt; &lt;/span&gt;-mp&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It completed the initial routing in 10 passes, then spent another 49 passes optimizing trace length, converging at pass 59 with the message: &lt;em&gt;"There were only 10.60 track length increase in the last 5 passes, so it's very likely that autorouter can't improve the result further."&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Total routing time: about three minutes. The result: 191 wires decomposed into 897 individual trace segments, plus 82 vias for layer transitions. Every net connected. Every design rule satisfied.&lt;/p&gt;
&lt;h4&gt;Importing Routes Back&lt;/h4&gt;
&lt;p&gt;One more headless problem: pcb-rnd's SES import requires the GUI. I tried &lt;code&gt;xvfb-run&lt;/code&gt; with action commands, but it hung waiting for GTK widget interactions that couldn't happen without a display.&lt;/p&gt;
&lt;p&gt;The solution was yet another AI-generated Python script (&lt;code&gt;ses_to_pcb.py&lt;/code&gt;) that parsed the Freerouting SES output and injected the routes directly into the PCB file as copper Line entries. The main complication was coordinate system conversion: the SES file uses a bottom-left origin (y increases upward) while pcb-rnd uses a top-left origin (y increases downward). The script also handled via translation, mapping Freerouting's via definitions to pcb-rnd's format with appropriate pad sizes, drill diameters, and clearances.&lt;/p&gt;
&lt;p&gt;897 trace segments and 82 vias injected. The PCB was fully routed.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/dual-z80/top-copper.png" alt="Top copper layer of the dual Z80 RetroShield PCB viewed in Gerber Viewer, showing 897 autorouted trace segments and 82 vias connecting both CPUs to the shared bus" style="width: 100%; max-width: 800px; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); margin: 1.5em 0;"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The top copper layer after Freerouting: 897 trace segments connecting 48 nets across both Z80s, the J1 bus header, and the J2 control header. Every trace was placed by the autorouter; none were drawn by hand.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Generating Production Files&lt;/h3&gt;
&lt;p&gt;The final step was generating Gerber files, the industry-standard format that PCB fabrication houses use to manufacture boards. pcb-rnd's command-line exporter handled this cleanly:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;pcb-rnd&lt;span class="w"&gt; &lt;/span&gt;-x&lt;span class="w"&gt; &lt;/span&gt;gerber&lt;span class="w"&gt; &lt;/span&gt;--all-layers&lt;span class="w"&gt; &lt;/span&gt;kz80.pcb
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This produced 11 files covering top and bottom copper, solder mask, silkscreen, paste stencil, board outline, and drill locations. pcb-rnd uses verbose filenames (&lt;code&gt;kz80.top.copper.none.3.gbr&lt;/code&gt;), so a renaming script converted them to the standard extensions (&lt;code&gt;.gtl&lt;/code&gt;, &lt;code&gt;.gbl&lt;/code&gt;, &lt;code&gt;.gts&lt;/code&gt;, etc.) that fabrication houses expect.&lt;/p&gt;
&lt;p&gt;I also added &lt;code&gt;tinycomputers.io&lt;/code&gt; to the top silkscreen layer, placed directly below the existing &lt;code&gt;www.8bitforce.com&lt;/code&gt; text, a small nod to both projects.&lt;/p&gt;
&lt;p&gt;The final Gerber package: 35KB zipped, ready for fabrication.&lt;/p&gt;
&lt;h3&gt;The Final Board&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/dual-z80/silkscreen.png" alt="Top silkscreen layer of the dual Z80 RetroShield PCB in Gerber Viewer, showing U1 and U2 Z80 CPU footprints, J1 and J2 headers, component labels, and tinycomputers.io branding" style="width: 100%; max-width: 800px; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); margin: 1.5em 0;"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The top silkscreen: U1 (left) and U2 (right) with the J1 bus header on the far left and the J2 control header between the two CPUs. The silkscreen includes the original 8bitforce.com credit alongside tinycomputers.io.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Here's what changed from the original RetroShield to the dual-CPU version:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Original&lt;/th&gt;
&lt;th&gt;Dual CPU&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Board dimensions&lt;/td&gt;
&lt;td&gt;55.88 × 53.34mm&lt;/td&gt;
&lt;td&gt;86.36 × 53.34mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Layers&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Z80 CPUs&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Headers&lt;/td&gt;
&lt;td&gt;J1 (36 pins)&lt;/td&gt;
&lt;td&gt;J1 (36) + J2 (12) = 48 pins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nets&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Through-hole components&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SMD components&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trace segments&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;897&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vias&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;82&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The board is wider but not taller. The second Z80 sits to the right of the first, with the J2 control header between them. Both CPUs share the J1 bus connection, and the Arduino firmware will manage who drives the bus at any given moment.&lt;/p&gt;
&lt;h3&gt;The Toolchain Nobody Uses&lt;/h3&gt;
&lt;p&gt;It's worth stepping back to note what just happened. An entire PCB was designed (schematic capture, component placement, autorouting, Gerber generation) without opening a single graphical application. Every step was either a command-line tool invocation or an AI-generated Python script manipulating text files. And it was done by someone who, at the start of the project, couldn't have told you the difference between a Gerber file and a drill file.&lt;/p&gt;
&lt;p&gt;That was the whole point. I chose to avoid a GUI specifically because I wanted to test a hypothesis: that AI-assisted, text-based workflows could let someone with domain knowledge in adjacent areas (firmware, systems programming) operate effectively in an unfamiliar domain (PCB design). The text-based EDA formats made this possible; they gave the AI something it could read, reason about, and generate. A graphical tool would have put me back to square one, clicking through menus I didn't understand.&lt;/p&gt;
&lt;p&gt;I'm not claiming this is &lt;em&gt;better&lt;/em&gt; than using KiCad or Altium with a mouse. For complex boards with hundreds of components, graphical tools and experienced designers are indispensable. But for a modification like this (adding a known set of components to an existing, well-documented open-source design), AI plus text-based tools was surprisingly effective. I brought the architectural understanding (how Z80 bus arbitration works, which signals need to be shared versus independent) and the AI handled the translation into file formats I'd never touched before. Most of the time was spent understanding the &lt;em&gt;design&lt;/em&gt;, not fighting tools.&lt;/p&gt;
&lt;h3&gt;What's Next&lt;/h3&gt;
&lt;p&gt;The Gerber files are at the fab now. In part two, I'll cover what happens when the physical boards arrive: inspection, assembly, first power-on, and the Arduino firmware that orchestrates two Z80s on a shared bus. The firmware is where the real complexity lives: bus arbitration timing, memory mapping for two independent address spaces, and the question of what to actually &lt;em&gt;run&lt;/em&gt; on a dual-Z80 system in 2026.&lt;/p&gt;
&lt;p&gt;Here's a preview of what the bus arbitration core looks like. The Arduino manages which CPU owns the shared bus at any given moment using the Z80's hardware BUSRQ/BUSAK handshake:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;// --- Pin definitions (active low) ---&lt;/span&gt;
&lt;span class="c1"&gt;// CPU1 control (directly from J1 via existing RetroShield mapping)&lt;/span&gt;
&lt;span class="cp"&gt;#define CPU1_CLK      A5&lt;/span&gt;
&lt;span class="cp"&gt;#define CPU1_BUSRQ    A4    &lt;/span&gt;&lt;span class="c1"&gt;// directly from Arduino to CPU1 BUSRQ pin&lt;/span&gt;
&lt;span class="cp"&gt;#define CPU1_BUSAK    A3    &lt;/span&gt;&lt;span class="c1"&gt;// directly from CPU1 BUSAK pin to Arduino&lt;/span&gt;

&lt;span class="c1"&gt;// CPU2 control (directly from J2 header)&lt;/span&gt;
&lt;span class="cp"&gt;#define CPU2_CLK      2&lt;/span&gt;
&lt;span class="cp"&gt;#define CPU2_BUSRQ    3&lt;/span&gt;
&lt;span class="cp"&gt;#define CPU2_BUSAK    4&lt;/span&gt;

&lt;span class="c1"&gt;// Bus state&lt;/span&gt;
&lt;span class="k"&gt;volatile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;active_cpu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// BUSRQ is output (Arduino tells CPU to release bus)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;pinMode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CPU1_BUSRQ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;OUTPUT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;pinMode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CPU2_BUSRQ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;OUTPUT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// BUSAK is input (CPU tells Arduino it released bus)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;pinMode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CPU1_BUSAK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;INPUT_PULLUP&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;pinMode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CPU2_BUSAK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;INPUT_PULLUP&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Start with CPU1 active, CPU2 off the bus&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;digitalWrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CPU1_BUSRQ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HIGH&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;// HIGH = don't request bus release&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;digitalWrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CPU2_BUSRQ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LOW&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// LOW  = request CPU2 to release bus&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;active_cpu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Wait for CPU2 to acknowledge it's off the bus&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digitalRead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CPU2_BUSAK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HIGH&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;switch_to_cpu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cpu&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cpu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;active_cpu&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;old_busrq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;active_cpu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CPU1_BUSRQ&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CPU2_BUSRQ&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;old_busak&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;active_cpu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CPU1_BUSAK&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CPU2_BUSAK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;new_busrq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cpu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CPU1_BUSRQ&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CPU2_BUSRQ&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Ask the active CPU to release the bus&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;digitalWrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old_busrq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LOW&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Wait for acknowledgment (CPU finishes current machine cycle first)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="kt"&gt;unsigned&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;micros&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digitalRead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old_busak&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HIGH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;micros&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;// hung CPU — shouldn't happen&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Bus is free. Release the new CPU onto it.&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;digitalWrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_busrq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HIGH&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;active_cpu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cpu&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The critical detail is timing. When the Arduino pulls BUSRQ low, the Z80 doesn't stop immediately; it finishes its current machine cycle, which can take 3–6 clock periods depending on the instruction. Only then does it tristate its address, data, and control outputs and assert BUSAK. The &lt;code&gt;while&lt;/code&gt; loop waits for that handshake to complete. During the transition, neither CPU is driving the bus, and the Arduino must not attempt any bus operations.&lt;/p&gt;
&lt;p&gt;This is a simplified version. The full firmware in part two will handle clock generation for both CPUs, memory mapping, I/O dispatch, and the arbitration policy (round-robin, priority-based, or cooperative yield). But the handshake above is the foundation everything else builds on. It's the same protocol that made multi-Z80 S-100 systems work in the early 1980s.&lt;/p&gt;
&lt;p&gt;The hardware design is the easy part. Making two 50-year-old processors cooperate is the challenge.&lt;/p&gt;
&lt;h3&gt;Source Files&lt;/h3&gt;
&lt;p&gt;All schematics, PCB files, Gerber outputs, and helper scripts for this project are open source:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://baud.rs/i4XqDV"&gt;dual-z80&lt;/a&gt;&lt;/strong&gt;: KiCad/gEDA source files, Gerber package, Python scripts for PCB manipulation, and build log&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;This is part one of a two-part series. Part two will cover board assembly, bring-up, and dual-CPU firmware.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Previous RetroShield posts: &lt;a href="https://tinycomputers.io/posts/cpm-on-physical-retroshield-z80.html"&gt;CP/M on the RetroShield&lt;/a&gt; · &lt;a href="https://tinycomputers.io/posts/fiverr-pcb-design-arduino-giga-shield.html"&gt;Fiverr PCB Design&lt;/a&gt; · &lt;a href="https://tinycomputers.io/posts/cpm-on-arduino-giga-r1-wifi.html"&gt;CP/M on the Giga R1&lt;/a&gt; · &lt;a href="https://tinycomputers.io/posts/zork-on-retroshield-z80-arduino-giga.html"&gt;Zork on the Giga&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</description><category>arduino</category><category>dual cpu</category><category>freerouting</category><category>geda</category><category>gerber</category><category>hardware</category><category>lepton-eda</category><category>multiprocessor</category><category>pcb design</category><category>pcb-rnd</category><category>retro computing</category><category>retroshield</category><category>z80</category><guid>https://tinycomputers.io/posts/designing-a-dual-z80-retroshield-part-1.html</guid><pubDate>Fri, 06 Mar 2026 14:00:00 GMT</pubDate></item><item><title>An LLM Clean Room Z80 Emulator: Building from Specifications, Not Source Code</title><link>https://tinycomputers.io/posts/clean-room-z80-emulator.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/clean-room-z80-emulator_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;43 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;&lt;img src="https://tinycomputers.io/images/clean-room-z80-emulator/zilog-z80.jpg" alt="An original Zilog Z80 CPU in a white ceramic DIP-40 package, manufactured in Dallas, 1976" style="float: right; max-width: 300px; margin: 0 0 1em 1.5em; border-radius: 4px; box-shadow: 0 10px 20px rgba(0,0,0,.1);" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;There's a particular kind of satisfaction in building something from specifications rather than from someone else's implementation. When you take timing diagrams and instruction tables and turn them into working code, you're not copying; you're reconstructing. Every decision about how to decode an opcode, how to handle the flag register's undocumented bits, how to sequence a block transfer instruction: these become deliberate choices, informed by the original engineering documents but filtered through genuine understanding of the problem.&lt;/p&gt;
&lt;p&gt;This is the story of writing a complete Z80 emulator under clean room constraints, with the twist that the implementer is an LLM. I used &lt;a href="https://baud.rs/claude-code"&gt;Claude Code&lt;/a&gt; to write every line of C (the CPU core, the test suite, the system emulator) with a single, non-negotiable rule: &lt;strong&gt;no reference to existing Z80 emulator source code&lt;/strong&gt;. The inputs were the &lt;a href="https://baud.rs/EESjG1"&gt;Zilog Z80 CPU User Manual&lt;/a&gt;, my architectural plan, and the test ROMs to prove it works.&lt;/p&gt;
&lt;p&gt;An important clarification on what "clean room" means here. The constraint was no existing emulator source code, not "only the official Zilog manual." The Z80's undocumented behaviors (the F3/F5 flag bits, IXH/IXL half-index registers, DDCB register copy side effects) aren't in Zilog's official documentation. They come from decades of community reverse-engineering documented in references like Sean Young's "&lt;a href="https://baud.rs/s0MAzk"&gt;The Undocumented Z80 Documented&lt;/a&gt;" and similar technical write-ups. Claude's training data includes this secondary documentation, and the clean room constraint didn't prohibit drawing on that knowledge; it prohibited referencing how &lt;em&gt;other emulators implemented&lt;/em&gt; that knowledge. The distinction matters: a specification of behavior is not the same as someone else's code that implements it.&lt;/p&gt;
&lt;h3&gt;Why an LLM Clean Room?&lt;/h3&gt;
&lt;p&gt;The term "clean room" comes from the semiconductor and software industries, where it describes a development methodology designed to produce implementations that are legally and intellectually independent of existing ones. In the chip fabrication sense, it's a literal dust-free environment. In the software sense, it means building from specifications and documentation without ever examining existing implementations.&lt;/p&gt;
&lt;p&gt;When an LLM writes code, there's always the question: is this implementation derived from the specification I gave it, or is it pattern-matching against emulator source code in its training data? This is the central tension of using AI for systems programming. An LLM has likely seen dozens of Z80 emulators during training. If you just ask it to "write a Z80 emulator," you'll get something that works, but you can't know whether it's an original implementation or a recombination of memorized code.&lt;/p&gt;
&lt;p&gt;The clean room constraint changes the experiment. By explicitly instructing Claude that this is a clean room project (that all implementation must be derived solely from specifications and documentation, not from existing emulator source code), you're testing whether the model can work from first principles rather than from pattern recall. Can it read an instruction set specification, understand the semantics of each opcode, and produce correct flag computations without cribbing from someone else's &lt;code&gt;z80.c&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;Antirez explored this territory recently with his &lt;a href="https://baud.rs/Gwet5H"&gt;own Z80 emulator project&lt;/a&gt;, using Claude Code to generate a working ZX Spectrum emulator. His experiment demonstrated something important about LLM-assisted development: that providing an agent with proper specifications and documentation, rather than asking it to regurgitate training data, produces implementations that are genuinely novel assemblies of knowledge rather than memorized patterns. The code Claude produced for antirez passed the notoriously thorough ZEXALL test suite, validating every documented Z80 behavior including the undocumented flag bits. Antirez's conclusion was that the LLM wasn't decompressing training data; it was &lt;em&gt;assembling knowledge&lt;/em&gt;, the way a human developer would when working from a datasheet.&lt;/p&gt;
&lt;p&gt;Reading antirez's write-up was the catalyst for this project. I wanted to see whether the same approach (specifications in, working emulator out, clean room constraints enforced throughout) would hold up when I drove the process myself. The Z80 User Manual is one of the best-documented processor specifications ever written. Everything you need to build a working emulator is in that document. The question is whether an LLM, given that document as its source of truth and told not to reference existing implementations, can produce something correct.&lt;/p&gt;
&lt;h3&gt;The Process&lt;/h3&gt;
&lt;p&gt;The workflow looked nothing like "prompt and pray." I started by writing a detailed architectural plan: the CPU state struct layout, the instruction decoding strategy (bit field decomposition), the system emulator's responsibilities, the test coverage targets. This plan became Claude's specification, not just "write a Z80 emulator" but "implement the Z80 CPU using x/y/z/p/q bit field decoding of the opcode byte, with these specific callback interfaces, these T-state timing requirements, and this test structure."&lt;/p&gt;
&lt;p&gt;Claude then implemented each component: &lt;code&gt;z80.h&lt;/code&gt; first, then the full &lt;code&gt;z80.c&lt;/code&gt; CPU core, then the test suite, then the system emulator. I reviewed each piece, ran the tests, identified failures, and fed the errors back. The first compile had a T-state timing issue with DD/FD prefixed instructions; the prefix overhead was being double-counted. One test out of 117 failed. Claude diagnosed the problem (the prefix dispatch was adding 4 T-states on top of instruction timings that already included the prefix cost) and fixed it.&lt;/p&gt;
&lt;p&gt;This iterative loop (plan, implement, test, fix) is exactly how a human developer would work. The difference is velocity. The entire CPU core, all 1,300 lines of C covering every official Z80 instruction plus undocumented behaviors, was produced in a single session. A human developer working from the same specification would spend days or weeks reaching the same point. The LLM's advantage isn't that it knows more; it's that it can hold the entire instruction set specification in context and translate it to code without the cognitive overhead of context-switching between the manual and the editor.&lt;/p&gt;
&lt;h3&gt;The Architecture&lt;/h3&gt;
&lt;p&gt;What Claude produced is a four-file emulator:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;z80.h&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CPU state struct, flag constants, public API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;z80.c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Complete Z80 CPU emulation core&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;z80_test.c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;117 unit tests covering all instruction groups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zxs.c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unified emulator binary with ACIA serial and CP/M support&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The design philosophy is straightforward: the CPU core knows nothing about the system it's running in. It communicates with the outside world exclusively through four callback functions: memory read, memory write, I/O in, and I/O out. The system emulator (&lt;code&gt;zxs.c&lt;/code&gt;) provides these callbacks and implements whatever hardware peripherals the target system requires.&lt;/p&gt;
&lt;p&gt;This separation matters. The same CPU core can run a Grant Searle BASIC SBC, an RC2014, or a CP/M program without any changes to &lt;code&gt;z80.c&lt;/code&gt;. The system-specific behavior lives entirely in the callbacks.&lt;/p&gt;
&lt;h3&gt;The CPU State&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/clean-room-z80-emulator/z80-architecture.png" alt="Z80 CPU architecture block diagram showing the register file with main and shadow registers, ALU, instruction decoder, and 8-bit data bus / 16-bit address bus" style="max-width: 100%; margin: 0 0 1em 0; border-radius: 4px; box-shadow: 0 10px 20px rgba(0,0,0,.1);" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;A Z80 has more programmer-visible state than you might expect if you're used to simpler processors. The main register file includes the accumulator &lt;code&gt;A&lt;/code&gt; and flags register &lt;code&gt;F&lt;/code&gt;, plus three general-purpose register pairs &lt;code&gt;BC&lt;/code&gt;, &lt;code&gt;DE&lt;/code&gt;, and &lt;code&gt;HL&lt;/code&gt;. But then there's a complete &lt;em&gt;shadow&lt;/em&gt; set of all those registers (&lt;code&gt;A'&lt;/code&gt;, &lt;code&gt;F'&lt;/code&gt;, &lt;code&gt;BC'&lt;/code&gt;, &lt;code&gt;DE'&lt;/code&gt;, &lt;code&gt;HL'&lt;/code&gt;), accessible only through the &lt;code&gt;EX AF,AF'&lt;/code&gt; and &lt;code&gt;EXX&lt;/code&gt; exchange instructions.&lt;/p&gt;
&lt;p&gt;Add the two 16-bit index registers &lt;code&gt;IX&lt;/code&gt; and &lt;code&gt;IY&lt;/code&gt;, the stack pointer &lt;code&gt;SP&lt;/code&gt;, the program counter &lt;code&gt;PC&lt;/code&gt;, the interrupt vector register &lt;code&gt;I&lt;/code&gt;, and the memory refresh counter &lt;code&gt;R&lt;/code&gt;, and you've got a substantial amount of state to track. Then there's the interrupt system: two flip-flops &lt;code&gt;IFF1&lt;/code&gt; and &lt;code&gt;IFF2&lt;/code&gt;, the interrupt mode register (modes 0, 1, or 2), a halt flag, and a one-instruction delay flag for the &lt;code&gt;EI&lt;/code&gt; instruction.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;typedef&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="cm"&gt;/* Main registers */&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;H&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="cm"&gt;/* Shadow registers */&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;A_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;F_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;B_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;C_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;D_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;E_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;H_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;L_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="cm"&gt;/* Index registers */&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint16_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;IX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;IY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="cm"&gt;/* Stack pointer and program counter */&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint16_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="cm"&gt;/* Interrupt and refresh registers */&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="cm"&gt;/* Interrupt state */&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;IFF1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;IFF2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;IM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;halted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ei_delay&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="cm"&gt;/* Cycle counter */&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;unsigned&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;t_states&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="cm"&gt;/* Memory and I/O callbacks */&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;z80_read_fn&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;mem_read&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;z80_write_fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mem_write&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;z80_in_fn&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;io_in&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;z80_out_fn&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;io_out&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;z80_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I chose to store each register as an individual byte rather than using unions or bitfields to form 16-bit pairs. This makes the code more explicit; when you see &lt;code&gt;cpu-&amp;gt;B&lt;/code&gt;, you know exactly what's being accessed. Register pairs are assembled and disassembled through inline helper functions like &lt;code&gt;rp_bc()&lt;/code&gt; and &lt;code&gt;set_bc()&lt;/code&gt;. The compiler optimizes these away completely, so there's no performance cost for the clarity.&lt;/p&gt;
&lt;h3&gt;Instruction Decoding: The Bit Field Approach&lt;/h3&gt;
&lt;p&gt;The Z80's instruction encoding has a structure that isn't immediately obvious if you're just looking at an opcode table, but becomes clear once you read the User Manual carefully. Every opcode byte can be decomposed into bit fields:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;x&lt;/code&gt; = bits 7:6 (the two highest bits)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;y&lt;/code&gt; = bits 5:3 (the middle three bits)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;z&lt;/code&gt; = bits 2:0 (the lowest three bits)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p&lt;/code&gt; = bits 5:4 (y &amp;gt;&amp;gt; 1)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;q&lt;/code&gt; = bit 3 (y &amp;amp; 1)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These fields determine the instruction's category and operands. For the unprefixed opcodes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;x=0&lt;/strong&gt;: Miscellaneous: relative jumps, 16-bit loads, 16-bit arithmetic, INC/DEC, 8-bit loads with immediate data, and the accumulator rotate/DAA/CPL/SCF/CCF group&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;x=1&lt;/strong&gt;: Register-to-register loads (&lt;code&gt;LD r, r'&lt;/code&gt;), with the special case of &lt;code&gt;LD (HL),(HL)&lt;/code&gt; encoding &lt;code&gt;HALT&lt;/code&gt; instead&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;x=2&lt;/strong&gt;: ALU operations between the accumulator and a register (&lt;code&gt;ADD A,r&lt;/code&gt; through &lt;code&gt;CP r&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;x=3&lt;/strong&gt;: Returns, jumps, calls, stack operations, RST vectors, I/O, exchange instructions, interrupt control, and the prefix bytes for extended instruction groups&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This structure means you can decode most unprefixed instructions with a three-level switch on x, then z (or y), rather than a 256-entry lookup table. The code reads more like the specification:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;switch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="cm"&gt;/* HALT */&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;halted&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;PC&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="cm"&gt;/* LD r[y], r[z] */&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;set_reg8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;get_reg8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="cm"&gt;/* ALU A, r[z] */&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;do_alu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;get_reg8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The register index mapping (0=B, 1=C, 2=D, 3=E, 4=H, 5=L, 6=(HL), 7=A) is used consistently throughout the instruction set. Index 6 always means the memory byte pointed to by &lt;code&gt;HL&lt;/code&gt;, which is why &lt;code&gt;LD (HL),(HL)&lt;/code&gt; would be meaningless (load memory from the same memory location) and gets repurposed as &lt;code&gt;HALT&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;The Prefix System&lt;/h3&gt;
&lt;p&gt;The Z80 extends its instruction set through prefix bytes: &lt;code&gt;CB&lt;/code&gt;, &lt;code&gt;ED&lt;/code&gt;, &lt;code&gt;DD&lt;/code&gt;, and &lt;code&gt;FD&lt;/code&gt;. Each opens up a different dimension of functionality.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CB prefix&lt;/strong&gt;: Rotate/shift operations and bit manipulation. The same x/y/z decode applies, but now x=0 is rotate/shift, x=1 is &lt;code&gt;BIT&lt;/code&gt; (test), x=2 is &lt;code&gt;RES&lt;/code&gt; (reset), and x=3 is &lt;code&gt;SET&lt;/code&gt;. This gives you eight different rotate/shift operations on any of the eight register positions, and bit test/set/reset for any of eight bit positions on any register. That's 248 instructions from a single prefix byte.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ED prefix&lt;/strong&gt;: Extended operations that don't fit the main opcode map. Block transfer and search instructions (&lt;code&gt;LDI&lt;/code&gt;, &lt;code&gt;LDIR&lt;/code&gt;, &lt;code&gt;LDD&lt;/code&gt;, &lt;code&gt;LDDR&lt;/code&gt;, &lt;code&gt;CPI&lt;/code&gt;, &lt;code&gt;CPIR&lt;/code&gt;, and their output counterparts), 16-bit arithmetic with carry (&lt;code&gt;ADC HL,rp&lt;/code&gt; and &lt;code&gt;SBC HL,rp&lt;/code&gt;), extended I/O (&lt;code&gt;IN r,(C)&lt;/code&gt; and &lt;code&gt;OUT (C),r&lt;/code&gt;), interrupt mode selection, and a handful of register transfer instructions (&lt;code&gt;LD I,A&lt;/code&gt;, &lt;code&gt;LD A,R&lt;/code&gt;, etc.).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DD and FD prefixes&lt;/strong&gt;: These modify the &lt;em&gt;following&lt;/em&gt; instruction by replacing &lt;code&gt;HL&lt;/code&gt; with &lt;code&gt;IX&lt;/code&gt; or &lt;code&gt;IY&lt;/code&gt; respectively. Wherever the unprefixed instruction uses &lt;code&gt;HL&lt;/code&gt; as a 16-bit register, the prefixed version uses &lt;code&gt;IX&lt;/code&gt; or &lt;code&gt;IY&lt;/code&gt;. Wherever it accesses &lt;code&gt;(HL)&lt;/code&gt; as a memory operand, the prefixed version accesses &lt;code&gt;(IX+d)&lt;/code&gt; or &lt;code&gt;(IY+d)&lt;/code&gt;, where &lt;code&gt;d&lt;/code&gt; is a signed displacement byte inserted between the opcode and any immediate data.&lt;/p&gt;
&lt;p&gt;This substitution extends to the individual &lt;code&gt;H&lt;/code&gt; and &lt;code&gt;L&lt;/code&gt; registers in many contexts. &lt;code&gt;LD A,H&lt;/code&gt; becomes &lt;code&gt;LD A,IXH&lt;/code&gt; with a DD prefix. These "half-index" register operations are technically undocumented but universally supported by real silicon and widely used by software. A clean room implementation needs to handle them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DDCB and FDCB&lt;/strong&gt;: The most complex prefix combination. For bit operations on indexed memory &lt;code&gt;(IX+d)&lt;/code&gt;, the displacement byte comes &lt;em&gt;before&lt;/em&gt; the opcode byte, not after:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;DD CB d op    →    operation on (IX+d)
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This reversed order exists because the Z80's internal pipeline needs the displacement early to begin the memory access while decoding the operation. It's an elegant microarchitectural optimization that reveals itself in the instruction encoding.&lt;/p&gt;
&lt;p&gt;There's an additional subtlety: undocumented behavior where DDCB/FDCB rotate and set/reset operations also copy their result into a register specified by the &lt;code&gt;z&lt;/code&gt; field of the opcode. &lt;code&gt;RLC (IX+5)&lt;/code&gt; with a &lt;code&gt;z&lt;/code&gt; field of 0 also loads the result into &lt;code&gt;B&lt;/code&gt;. This behavior is consistent across all real Z80 chips and is relied upon by some software.&lt;/p&gt;
&lt;h3&gt;The ALU&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/clean-room-z80-emulator/z80-die-shot.jpg" alt="High-resolution die photograph of a Zilog Z80 CPU showing the silicon layout of the ALU, register file, and control logic" style="float: right; max-width: 40%; margin: 0 0 1em 1.5em; border-radius: 4px; box-shadow: 0 10px 20px rgba(0,0,0,.1);" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;The eight ALU operations (ADD, ADC, SUB, SBC, AND, XOR, OR, CP) share a common pattern in how they affect the flags register. Getting the flags right is the single most important aspect of Z80 emulation, and the area where most subtle bugs hide.&lt;/p&gt;
&lt;p&gt;The Z80's flag register contains eight bits, six of which are documented:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bit&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;S&lt;/td&gt;
&lt;td&gt;Sign (copy of bit 7 of result)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Z&lt;/td&gt;
&lt;td&gt;Zero (result is zero)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;F5&lt;/td&gt;
&lt;td&gt;Undocumented (copy of bit 5 of result*)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;H&lt;/td&gt;
&lt;td&gt;Half-carry (carry from bit 3 to bit 4)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;F3&lt;/td&gt;
&lt;td&gt;Undocumented (copy of bit 3 of result*)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;P/V&lt;/td&gt;
&lt;td&gt;Parity (logic ops) or Overflow (arithmetic ops)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;Subtract (set if last operation was subtraction)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;Carry (carry out of bit 7)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The asterisk on F3 and F5 matters. For most operations, bits 3 and 5 come from the &lt;em&gt;result&lt;/em&gt;. But for &lt;code&gt;CP&lt;/code&gt; (compare), they come from the &lt;em&gt;operand&lt;/em&gt;, not the result. This is because &lt;code&gt;CP&lt;/code&gt; is internally a subtraction that discards the result and keeps only the flags, but the Z80 designers connected the F3 and F5 flag inputs to the operand bus rather than the internal result bus for this particular instruction. It's the kind of detail that only shows up when you're testing against real hardware behavior.&lt;/p&gt;
&lt;p&gt;The overflow flag computation deserves special attention. For addition, overflow occurs when two operands of the same sign produce a result of the opposite sign:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x80&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For subtraction, overflow occurs when two operands of different signs produce a result whose sign differs from the first operand:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;These one-liners replace what would otherwise be multi-branch conditional logic. They work because XOR detects sign differences, and AND combines the two conditions.&lt;/p&gt;
&lt;h3&gt;Block Operations&lt;/h3&gt;
&lt;p&gt;The Z80's block instructions are one of its most powerful features and one of the trickiest to implement correctly. &lt;code&gt;LDIR&lt;/code&gt; (Load, Increment, Repeat) copies a block of memory from the address in &lt;code&gt;HL&lt;/code&gt; to the address in &lt;code&gt;DE&lt;/code&gt;, decrementing &lt;code&gt;BC&lt;/code&gt; as a counter, and repeating until &lt;code&gt;BC&lt;/code&gt; reaches zero.&lt;/p&gt;
&lt;p&gt;The implementation requires careful attention to the repeat mechanism. When &lt;code&gt;BC&lt;/code&gt; is not yet zero, &lt;code&gt;LDIR&lt;/code&gt; decrements &lt;code&gt;PC&lt;/code&gt; by 2 so that the next instruction fetch re-executes the same &lt;code&gt;LDIR&lt;/code&gt; opcode. The repeated iteration takes 21 T-states; the final iteration (when BC reaches zero) takes only 16 T-states. This asymmetry matters for cycle-accurate emulation:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cm"&gt;/* LDI/LDD/LDIR/LDDR */&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rp_hl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;wb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rp_de&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="cm"&gt;/* Increment or decrement based on instruction */&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;set_hl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rp_hl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;set_de&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rp_de&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;set_hl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rp_hl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;set_de&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rp_de&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;set_bc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rp_bc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="cm"&gt;/* ... flag computation ... */&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rp_bc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;PC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The flag behavior during block operations is another area where the specification requires careful reading. The &lt;code&gt;P/V&lt;/code&gt; flag reflects whether &lt;code&gt;BC&lt;/code&gt; is non-zero after the decrement, acting as a "more data" indicator. The undocumented F3 and F5 flags come from the sum of the transferred byte and the accumulator, with F5 derived from bit 1 rather than bit 5 of that sum. These details are well-documented in the secondary literature but require careful implementation.&lt;/p&gt;
&lt;p&gt;The search variants (&lt;code&gt;CPI&lt;/code&gt;, &lt;code&gt;CPIR&lt;/code&gt;, etc.) are even more nuanced. They compare the accumulator against memory, set Z if a match is found, and terminate on either a match or &lt;code&gt;BC&lt;/code&gt; reaching zero. The flags after a search operation encode both whether a match was found &lt;em&gt;and&lt;/em&gt; whether the counter has been exhausted, two independent pieces of information packed into the flag register.&lt;/p&gt;
&lt;h3&gt;T-State Timing&lt;/h3&gt;
&lt;p&gt;Every Z80 instruction has a specific T-state (clock cycle) count that's documented in the User Manual. For an emulator driving a simulated UART or polling for terminal input at realistic intervals, accurate timing is essential.&lt;/p&gt;
&lt;p&gt;The timing model uses a simple accumulator. Each call to &lt;code&gt;z80_step()&lt;/code&gt; returns the number of T-states consumed and adds them to a running total in the CPU state. The system emulator uses this to determine when to poll for input or deliver interrupts:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;quit_flag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;unsigned&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cpu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;t_states&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7373&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cpu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;t_states&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;z80_step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cpu&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="cm"&gt;/* Poll for serial input, deliver interrupts */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The value 7373 represents approximately 2 milliseconds at 3.6864 MHz, the crystal frequency used by many Z80 SBC designs. This frequency was chosen historically because it divides cleanly to produce standard baud rates. At 9600 baud with 10 bits per character (start, 8 data, stop), you get approximately 960 characters per second, or about one character every 3,840 clock cycles. Polling at 7373-cycle intervals gives roughly two opportunities to check for input per character time, enough for reliable serial communication without excessive overhead.&lt;/p&gt;
&lt;p&gt;Conditional instructions have different cycle counts depending on whether the condition is met. A &lt;code&gt;JR Z,d&lt;/code&gt; takes 12 T-states when the jump is taken but only 7 when it falls through. &lt;code&gt;CALL cc,nn&lt;/code&gt; takes 17 T-states when taken, 10 when not. These differences reflect the real pipeline behavior of the Z80; a taken branch requires additional cycles to flush the prefetch and load the new address.&lt;/p&gt;
&lt;h3&gt;The Interrupt System&lt;/h3&gt;
&lt;p&gt;The Z80 supports three interrupt modes and a non-maskable interrupt. Mode 1 is the simplest and most commonly used in SBC designs: a maskable interrupt causes the CPU to push the current PC and jump to address &lt;code&gt;0x0038&lt;/code&gt;, just like an &lt;code&gt;RST 38h&lt;/code&gt; instruction.&lt;/p&gt;
&lt;p&gt;Mode 2 is more sophisticated. The interrupting device places a vector byte on the data bus, which is combined with the &lt;code&gt;I&lt;/code&gt; register to form a 16-bit address into a vector table in memory. The CPU reads the actual interrupt service routine address from that table location. This provides up to 128 different interrupt vectors, enabling complex multi-device interrupt schemes.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;EI&lt;/code&gt; instruction has a subtle but critical behavior: it doesn't enable interrupts immediately. Instead, it sets a one-instruction delay, so the &lt;em&gt;next&lt;/em&gt; instruction after &lt;code&gt;EI&lt;/code&gt; executes before any pending interrupt can be serviced. This guarantees that &lt;code&gt;EI; RETI&lt;/code&gt; (enable interrupts, then return from interrupt) executes atomically; the return completes before any new interrupt can preempt it.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cm"&gt;/* EI */&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;IFF1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;IFF2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ei_delay&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And in the interrupt handler:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;z80_interrupt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;z80_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cpu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;cpu&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;IFF1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cpu&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ei_delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="cm"&gt;/* ... process interrupt ... */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;DAA: The Most Misunderstood Instruction&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;DAA&lt;/code&gt; (Decimal Adjust Accumulator) is arguably the Z80's most complex single instruction. It adjusts the result of a previous addition or subtraction to produce a valid BCD (Binary-Coded Decimal) result. The adjustment depends on three pieces of state: the current value of the accumulator, the carry flag, and the half-carry flag. It also behaves differently depending on whether the previous operation was addition or subtraction (tracked by the N flag).&lt;/p&gt;
&lt;p&gt;The algorithm: if the lower nibble exceeds 9 or the half-carry flag is set, add (or subtract) 0x06. If the upper nibble exceeds 9 or the carry flag is set, add (or subtract) 0x60. Update carry if the upper correction was applied.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;daa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;z80_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;correction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;carry&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Z80_CF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Z80_HF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x0F&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;correction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x06&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;carry&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x99&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;correction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;carry&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Z80_CF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Z80_NF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;correction&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;correction&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sz53p&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;carry&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Z80_NF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Z80_HF&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;BCD arithmetic was important in the era when the Z80 was designed. Financial calculations, display drivers, and industrial controllers all needed decimal precision without floating-point hardware. The Z80's DAA instruction made BCD arithmetic practical on an 8-bit processor by adjusting binary results back into valid decimal digits after each operation.&lt;/p&gt;
&lt;h3&gt;Testing: 117 Ways to Be Wrong&lt;/h3&gt;
&lt;p&gt;Writing a test suite for a CPU emulator is an exercise in paranoia. Every instruction has multiple paths through the flag logic, multiple edge cases in operand handling, and multiple interactions with the rest of the CPU state. The test suite covers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Register loads&lt;/strong&gt;: 8-bit immediate, register-to-register, 16-bit immediate, indirect through BC/DE/HL, absolute addressing, HL indirect&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;8-bit ALU&lt;/strong&gt;: All eight operations with basic values, carry/borrow propagation, overflow detection, half-carry, undocumented flag bits&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;16-bit arithmetic&lt;/strong&gt;: ADD HL with carry, SBC HL, ADC HL&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;INC/DEC&lt;/strong&gt;: 8-bit with overflow and half-carry edge cases, 16-bit wrapping&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rotates and shifts&lt;/strong&gt;: RLCA/RRCA/RLA/RRA (accumulator), CB-prefixed RLC/RRC/RL/RR/SLA/SRA/SRL on registers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BIT/SET/RES&lt;/strong&gt;: Test, set, and reset individual bits&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Jumps and branches&lt;/strong&gt;: JP, JR, DJNZ with taken/not-taken paths&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Calls and returns&lt;/strong&gt;: CALL/RET with condition codes, RST vectors&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stack operations&lt;/strong&gt;: PUSH/POP for all register pairs including AF&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Block operations&lt;/strong&gt;: LDI/LDIR/LDD, CPI/CPIR, INI/OUTI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exchange instructions&lt;/strong&gt;: EX AF, EXX, EX DE,HL, EX (SP),HL&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interrupt system&lt;/strong&gt;: IM modes, Mode 1 and Mode 2 dispatch, NMI, EI delay&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IX/IY indexed&lt;/strong&gt;: Loads, stores, arithmetic, IXH/IXL access, DDCB bit operations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;T-state timing&lt;/strong&gt;: Verified counts for representative instructions from each group&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;R register&lt;/strong&gt;: Increment behavior, bit 7 preservation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each test sets up a specific CPU state, loads a short instruction sequence into memory, executes it, and verifies the results. The test framework is minimal, just macros for assertions and a runner that reports pass/fail:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gh"&gt;Z80 CPU Test Suite&lt;/span&gt;
&lt;span class="gh"&gt;==================&lt;/span&gt;
test_nop                                                    PASS
&lt;span class="gh"&gt;test_ld_reg_imm                                             PASS&lt;/span&gt;
&lt;span class="gh"&gt;...&lt;/span&gt;
test_r_bit7_preserved                                       PASS

==================
Results: 117/117 passed
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;All 117 pass. But passing unit tests isn't the same as passing real software. The real validation comes from booting actual ROMs.&lt;/p&gt;
&lt;h3&gt;The System Emulator&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;zxs&lt;/code&gt; binary wraps the CPU core with enough peripheral emulation to run two classes of software: Grant Searle-style BASIC SBCs with ACIA serial I/O, and CP/M .COM programs with a minimal BDOS shim.&lt;/p&gt;
&lt;h4&gt;ACIA Serial Emulation&lt;/h4&gt;
&lt;p&gt;The Motorola MC6850 ACIA (Asynchronous Communications Interface Adapter) is the serial chip used in the Grant Searle Z80 SBC design and many similar projects. It presents two registers to the CPU:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Status register&lt;/strong&gt; (base address): Bit 0 = Receive Data Register Full (RDRF), Bit 1 = Transmit Data Register Empty (TDRE)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data register&lt;/strong&gt; (base + 1): Read for received data, write to transmit&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The emulation maps these to terminal I/O. TDRE is always set (the "transmitter" is always ready since we're writing directly to stdout). RDRF is set when non-blocking &lt;code&gt;read()&lt;/code&gt; has captured a character from stdin. The ACIA's interrupt capability is emulated: when receive interrupts are enabled and data is available, the emulator delivers an RST 38h interrupt to the CPU.&lt;/p&gt;
&lt;h4&gt;Serial Port Auto-Detection&lt;/h4&gt;
&lt;p&gt;Rather than hardcoding the ACIA port address, the emulator scans the loaded ROM for &lt;code&gt;IN A,(n)&lt;/code&gt; (&lt;code&gt;DB xx&lt;/code&gt;) and &lt;code&gt;OUT (n),A&lt;/code&gt; (&lt;code&gt;D3 xx&lt;/code&gt;) instruction patterns. It collects the referenced port addresses and looks for adjacent pairs (status + data ports) that have both IN and OUT references, the signature of a serial peripheral. For the Grant Searle ROM, this reliably detects port base &lt;code&gt;0x80&lt;/code&gt;. For ROMs that use different port configurations, a &lt;code&gt;--port&lt;/code&gt; flag provides a manual override.&lt;/p&gt;
&lt;h4&gt;CP/M Mode&lt;/h4&gt;
&lt;p&gt;For &lt;code&gt;.com&lt;/code&gt; and &lt;code&gt;.cim&lt;/code&gt; files, the emulator switches to CP/M mode: the program is loaded at &lt;code&gt;0x0100&lt;/code&gt;, the stack pointer is set to &lt;code&gt;0xFFFE&lt;/code&gt; with a return address of &lt;code&gt;0x0000&lt;/code&gt; pushed, and BDOS calls are intercepted at address &lt;code&gt;0x0005&lt;/code&gt;. Only the essential BDOS functions are implemented (console output (function 2) and string output (function 9)), but this is enough to run many CP/M utilities and test programs.&lt;/p&gt;
&lt;h4&gt;System Auto-Detection&lt;/h4&gt;
&lt;p&gt;File extension determines the system type: &lt;code&gt;.com&lt;/code&gt; and &lt;code&gt;.cim&lt;/code&gt; files run in CP/M mode, everything else runs as a BASIC SBC. Intel HEX files are detected and parsed regardless of extension. The &lt;code&gt;--system&lt;/code&gt; flag overrides auto-detection when needed.&lt;/p&gt;
&lt;h3&gt;Booting BASIC&lt;/h3&gt;
&lt;p&gt;The real test of any emulator is whether it runs real software. Here's what happens when you point &lt;code&gt;zxs&lt;/code&gt; at Grant Searle's BASIC ROM:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;./zxs&lt;span class="w"&gt; &lt;/span&gt;basic.rom
Loaded&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8192&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bytes&lt;span class="w"&gt; &lt;/span&gt;at&lt;span class="w"&gt; &lt;/span&gt;0x0000
BASIC&lt;span class="w"&gt; &lt;/span&gt;SBC&lt;span class="w"&gt; &lt;/span&gt;mode,&lt;span class="w"&gt; &lt;/span&gt;serial&lt;span class="w"&gt; &lt;/span&gt;port&lt;span class="w"&gt; &lt;/span&gt;base:&lt;span class="w"&gt; &lt;/span&gt;0x80&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;Ctrl+&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
Z80&lt;span class="w"&gt; &lt;/span&gt;SBC&lt;span class="w"&gt; &lt;/span&gt;By&lt;span class="w"&gt; &lt;/span&gt;Grant&lt;span class="w"&gt; &lt;/span&gt;Searle

Memory&lt;span class="w"&gt; &lt;/span&gt;top?
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That banner, "Z80 SBC By Grant Searle," represents thousands of Z80 instructions executing correctly. The ROM initializes memory, configures the ACIA, sets up the interrupt handler, and enters the BASIC interpreter's command loop. Each of those steps exercises a different subset of the CPU's instruction set. A single incorrectly implemented instruction (a wrong flag bit, a miscounted displacement, a botched stack operation) would cause the ROM to crash or produce garbage output.&lt;/p&gt;
&lt;p&gt;The RC2014 BASIC ROM boots as well, though it requires specifying the serial port base since its ROM references multiple I/O addresses:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;./zxs&lt;span class="w"&gt; &lt;/span&gt;--port&lt;span class="w"&gt; &lt;/span&gt;0x80&lt;span class="w"&gt; &lt;/span&gt;rc2014_56k.hex
Loaded&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8154&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bytes&lt;span class="w"&gt; &lt;/span&gt;from&lt;span class="w"&gt; &lt;/span&gt;HEX&lt;span class="w"&gt; &lt;/span&gt;file
BASIC&lt;span class="w"&gt; &lt;/span&gt;SBC&lt;span class="w"&gt; &lt;/span&gt;mode,&lt;span class="w"&gt; &lt;/span&gt;serial&lt;span class="w"&gt; &lt;/span&gt;port&lt;span class="w"&gt; &lt;/span&gt;base:&lt;span class="w"&gt; &lt;/span&gt;0x80&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;Ctrl+&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

RC2014&lt;span class="w"&gt; &lt;/span&gt;-&lt;span class="w"&gt; &lt;/span&gt;MS&lt;span class="w"&gt; &lt;/span&gt;Basic&lt;span class="w"&gt; &lt;/span&gt;Loader
z88dk&lt;span class="w"&gt; &lt;/span&gt;-&lt;span class="w"&gt; &lt;/span&gt;feilipu

Memory&lt;span class="w"&gt; &lt;/span&gt;top?
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Intel HEX file loading is handled transparently. The emulator detects the format by checking for the &lt;code&gt;:&lt;/code&gt; record marker and parses the standard Intel HEX record format (data records, EOF records, address fields, checksums).&lt;/p&gt;
&lt;h3&gt;What I Learned About LLM Clean Room Development&lt;/h3&gt;
&lt;p&gt;This project taught me as much about working with LLMs as it did about the Z80. Some observations:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Specification quality determines output quality.&lt;/strong&gt; When I gave Claude a vague instruction like "implement the Z80," the result would have been a generic emulator shaped by whatever training data dominates. When I gave it a detailed architectural plan (bit field decoding, specific callback interfaces, T-state requirements), the result was a coherent, well-structured implementation that reflected the design decisions in the specification. Antirez observed the same thing: the LLM performs dramatically better when you provide documentation and constraints rather than open-ended prompts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LLMs can work from datasheets, not just from memory.&lt;/strong&gt; The clean room constraint was the whole point: could Claude produce correct Z80 flag behavior, proper DDCB/FDCB displacement ordering, accurate block operation semantics, all derived from specification knowledge rather than memorized source code? The 117 passing tests and booting ROMs suggest it can. The code doesn't look like any particular existing emulator. The bit field decoder, the ALU structure, the prefix dispatch: these are architecturally reasonable but stylistically original.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The bug pattern was illuminating.&lt;/strong&gt; The one test failure in the initial implementation was a T-state timing issue: DD/FD prefix overhead was being double-counted. This is exactly the kind of bug a human developer would make when implementing prefix dispatch, a bookkeeping error at the boundary between the prefix handler and the main decoder. It was not the kind of error you'd see from copying existing code, where the timing would already be correct. The bug was &lt;em&gt;original&lt;/em&gt;, which paradoxically increases confidence that the implementation is too.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Z80's instruction encoding is remarkably systematic.&lt;/strong&gt; Once you express the x/y/z/p/q bit field decomposition in the architectural plan, the entire instruction set becomes a small number of patterns applied consistently across register indices and operation codes. Claude picked up on this structure immediately and produced a decoder that reads like the specification. The elegance of Zilog's encoding is invisible in an opcode table but obvious in a decoder, and an LLM can see that structure when pointed at it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The DD/FD prefix system is essentially a register renaming mechanism.&lt;/strong&gt; It doesn't introduce new operations; it modifies existing ones by replacing HL with IX or IY. Expressing this in the plan as "replace HL→IX/IY, H→IXH/IYH, L→IXL/IYL, (HL)→(IX+d)/(IY+d)" gave Claude the conceptual framework to implement DD/FD support as a modifier on the existing decoder rather than duplicating 200+ instruction handlers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Flag behavior is the specification.&lt;/strong&gt; Two Z80 emulators can produce identical results for every instruction and still differ in their flag register output. The undocumented F3 and F5 bits, the special CP flag behavior, the block instruction flag computations: these are what distinguish a correct emulator from an approximately correct one. Claude got the CP flag anomaly right (F3/F5 from the operand, not the result), which suggests it was working from specification knowledge about the Z80's internal bus routing rather than just copying a known-good flag computation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Clean room constraints make LLM output more trustworthy, not less.&lt;/strong&gt; There's an irony here: by &lt;em&gt;restricting&lt;/em&gt; what the LLM can reference, you get &lt;em&gt;more&lt;/em&gt; confidence in the result. If Claude had produced code that looked suspiciously like MAME's Z80 core, you'd wonder whether it was simply reciting training data. Instead, it produced an implementation that's structurally sound, stylistically distinct, and correct, the hallmarks of working from specifications rather than from examples.&lt;/p&gt;
&lt;h3&gt;The Code&lt;/h3&gt;
&lt;p&gt;The complete source is &lt;a href="https://baud.rs/Ae0K75"&gt;on GitHub&lt;/a&gt;, five files totaling roughly 3,000 lines of C. It builds with &lt;code&gt;make&lt;/code&gt;, produces zero warnings with &lt;code&gt;-Wall -Wextra&lt;/code&gt;, and runs Grant Searle and RC2014 BASIC ROMs out of the box.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;make
cc&lt;span class="w"&gt; &lt;/span&gt;-Wall&lt;span class="w"&gt; &lt;/span&gt;-Wextra&lt;span class="w"&gt; &lt;/span&gt;-O2&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;zxs&lt;span class="w"&gt; &lt;/span&gt;zxs.c&lt;span class="w"&gt; &lt;/span&gt;z80.c
cc&lt;span class="w"&gt; &lt;/span&gt;-Wall&lt;span class="w"&gt; &lt;/span&gt;-Wextra&lt;span class="w"&gt; &lt;/span&gt;-O2&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;z80_test&lt;span class="w"&gt; &lt;/span&gt;z80_test.c&lt;span class="w"&gt; &lt;/span&gt;z80.c

$&lt;span class="w"&gt; &lt;/span&gt;./z80_test
Z80&lt;span class="w"&gt; &lt;/span&gt;CPU&lt;span class="w"&gt; &lt;/span&gt;Test&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Suite&lt;/span&gt;
&lt;span class="o"&gt;==================&lt;/span&gt;
...
Results:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;117&lt;/span&gt;/117&lt;span class="w"&gt; &lt;/span&gt;passed

$&lt;span class="w"&gt; &lt;/span&gt;./zxs&lt;span class="w"&gt; &lt;/span&gt;basic.rom
Z80&lt;span class="w"&gt; &lt;/span&gt;SBC&lt;span class="w"&gt; &lt;/span&gt;By&lt;span class="w"&gt; &lt;/span&gt;Grant&lt;span class="w"&gt; &lt;/span&gt;Searle
Memory&lt;span class="w"&gt; &lt;/span&gt;top?
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There is more to do. ZEXALL compliance would be the next validation milestone; it tests every instruction against known-good results captured from real Z80 hardware. ZX Spectrum emulation would require adding ULA video, keyboard matrix scanning, and Spectrum-specific memory banking. Cycle-exact timing would enable accurate sound emulation and demo-scene effects.&lt;/p&gt;
&lt;p&gt;But for now, the ROM boots, BASIC runs, and every line of the emulator traces back to Z80 specifications and documentation rather than someone else's &lt;code&gt;z80.c&lt;/code&gt;. An LLM wrote it, but a human designed it, constrained it, tested it, and validated it against real hardware ROM images. The clean room constraint didn't just produce a trustworthy emulator; it produced a trustworthy &lt;em&gt;process&lt;/em&gt; for using LLMs on systems programming tasks. Give the model a specification instead of an open-ended prompt. Enforce constraints that prevent training data regurgitation. Validate against real-world artifacts, not just unit tests.&lt;/p&gt;
&lt;p&gt;Antirez asked whether LLMs create original code or decompress training data. This project is one more data point on the side of original creation, but only when you set up the conditions for it. The clean room is what makes the difference.&lt;/p&gt;</description><category>acia</category><category>ai</category><category>c</category><category>claude</category><category>clean room</category><category>cp/m</category><category>cpu design</category><category>emulation</category><category>grant searle</category><category>instruction decoding</category><category>llm</category><category>rc2014</category><category>retrocomputing</category><category>serial</category><category>z80</category><category>zilog</category><guid>https://tinycomputers.io/posts/clean-room-z80-emulator.html</guid><pubDate>Fri, 27 Feb 2026 00:00:00 GMT</pubDate></item><item><title>Playing Zork on a Real Z80: From CP/M Boot to the Great Underground Empire</title><link>https://tinycomputers.io/posts/zork-on-retroshield-z80-arduino-giga.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/zork-on-retroshield-z80-arduino-giga_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;16 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;This is the third post in a series about running CP/M 2.2 on a real Z80 processor connected to an Arduino Giga R1 WiFi. The &lt;a href="https://tinycomputers.io/posts/fiverr-pcb-design-arduino-giga-shield.html"&gt;first post&lt;/a&gt; covered getting the custom level converter shield designed and manufactured. The &lt;a href="https://tinycomputers.io/posts/cpm-on-arduino-giga-r1-wifi.html"&gt;second post&lt;/a&gt; documented the hardware stack, the catastrophic TXB0108 level converter failures, the shadow register workaround, and the Rust sector server that provides disk I/O over WiFi. That post ended with a promise: CP/M was close to booting, and all the pieces were in place.&lt;/p&gt;
&lt;p&gt;This post is about keeping that promise. It covers the final debugging push from "almost boots" to a fully interactive game of Zork I running on real Z80 hardware, and the performance crisis that nearly made the whole thing unusable.&lt;/p&gt;
&lt;h3&gt;The Story So Far&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/giga-shield-bare-pcb.jpeg" alt="The bare Arduino Giga R1 Shield V0.1 PCB, a red board with nine TXB0108 level converter ICs in antistatic packaging" style="float: right; width: 45%; max-width: 420px; margin: 0 0 1em 1.5em; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);"&gt;&lt;/p&gt;
&lt;p&gt;The hardware is straightforward: a &lt;a href="https://baud.rs/87wbBL"&gt;RetroShield Z80&lt;/a&gt; (a real &lt;a href="https://baud.rs/tFkBkH"&gt;Zilog Z80&lt;/a&gt; CPU on a shield board) plugged into an &lt;a href="https://baud.rs/poSQeo"&gt;Arduino Giga R1 WiFi&lt;/a&gt; through a custom level converter PCB. The Giga's STM32H747 (480MHz Cortex-M7) provides 64KB of Z80 RAM as a byte array in its internal SRAM, clocks the Z80, and serves memory read/write requests. Disk I/O goes over WiFi to a Rust TCP sector server instead of an SD card.&lt;/p&gt;
&lt;p&gt;The level converter uses nine &lt;a href="https://baud.rs/hY6ydl"&gt;TXB0108&lt;/a&gt; bidirectional level shifters to bridge the Giga's 3.3V logic and the RetroShield's 5V. And those TXB0108s are the source of almost every interesting engineering decision in the project. Their auto-direction sensing fails for several Z80 bus signals: &lt;code&gt;IORQ_N&lt;/code&gt; and &lt;code&gt;RD_N&lt;/code&gt; are permanently stuck, &lt;code&gt;WR_N&lt;/code&gt; only works during memory cycles, and the data bus is invisible from Z80-to-Arduino during I/O operations. The address bus works but lags by 1-3 clock ticks through the converter.&lt;/p&gt;
&lt;p&gt;These failures forced a fundamentally different approach to interfacing with the Z80. Instead of passively watching bus signals, the Arduino actively decodes the Z80's instruction stream and maintains software copies of the CPU's internal state:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Guard-only M1 detection&lt;/strong&gt;: a timing table (&lt;code&gt;tStates[256]&lt;/code&gt;) tells us how many clock cycles each instruction takes; the next memory read after the guard expires is the next opcode fetch&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Software PC (softPC)&lt;/strong&gt;: a software copy of the Z80's program counter, immune to address bus lag&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shadow registers&lt;/strong&gt;: software copies of A, B, C, D, E, H, L, F, and SP, updated by decoding each opcode from &lt;code&gt;z80RAM[softPC]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pre-writes&lt;/strong&gt;: memory store instructions write their values directly to &lt;code&gt;z80RAM&lt;/code&gt; at opcode detection time, using shadow register values and softPC-derived addresses, because the Z80's physical bus writes go to wrong addresses due to the propagation delay&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deferred writes&lt;/strong&gt;: for read-modify-write instructions like &lt;code&gt;INC (HL)&lt;/code&gt;, where pre-writing would cause the Z80 to read an already-modified value and double-apply the operation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The full technical details of this architecture are in the &lt;a href="https://tinycomputers.io/posts/cpm-on-arduino-giga-r1-wifi.html"&gt;previous post&lt;/a&gt;. What matters here is where that post left off: the shadow register system was working, the sector server was serving disk images over WiFi, and partial serial output confirmed that the Z80 was executing real code. What remained was completeness testing, making sure every instruction the Z80 actually executed was tracked correctly in the shadows.&lt;/p&gt;
&lt;h3&gt;CP/M Boots&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/giga-shield-assembled-top.jpeg" alt="The assembled stack: Arduino Giga R1 WiFi (blue) mounted on the red level converter PCB, with the RetroShield Z80 and its 40-pin Z80 DIP chip partially inserted on the right" style="float: left; width: 50%; max-width: 460px; margin: 0 1.5em 1em 0; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);"&gt;&lt;/p&gt;
&lt;p&gt;The first milestone came faster than expected. After expanding the shadow register switch statement to cover more of the Z80 instruction set (POP instructions, ADD HL with register pairs, DAA (decimal adjust), EX (SP),HL), CP/M booted.&lt;/p&gt;
&lt;p&gt;The boot loader loaded all 53 sectors of &lt;code&gt;CPM.SYS&lt;/code&gt; from the sector server over WiFi. The BIOS cold boot initialized correctly. And the console printed:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;RetroShield CP/M 2.2
56K TPA

a&amp;gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;A real Z80, running real &lt;a href="https://baud.rs/YxWgtr"&gt;CP/M 2.2&lt;/a&gt;, with 56KB of Transient Program Area, booting from a disk image served over WiFi from a Rust TCP server. The &lt;code&gt;DIR&lt;/code&gt; command worked and showed the contents of drive A:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;a&amp;gt;dir
A: ZORK1    COM : ZORK1    DAT : ZORK2    COM : ZORK2    DAT
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Zork was right there, waiting.&lt;/p&gt;
&lt;h3&gt;The "Bad Load" Bug&lt;/h3&gt;
&lt;p&gt;Running &lt;code&gt;ZORK1.COM&lt;/code&gt; produced a single line of output and then nothing:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;zork1&lt;/span&gt;
&lt;span class="n"&gt;Bad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;load&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;"Bad load" is a CP/M CCP (Console Command Processor) error. It means the CCP tried to load the .COM file into the TPA and something went wrong: either a disk read failed, or the CCP's internal logic decided the load was corrupt.&lt;/p&gt;
&lt;h4&gt;Finding the Root Cause&lt;/h4&gt;
&lt;p&gt;The CCP loads .COM files by repeatedly calling BDOS function 20 (Read Sequential), advancing the DMA address by 128 bytes after each successful sector read, until the file is fully loaded. The load loop lives in the CCP code at address &lt;code&gt;0xE6DE&lt;/code&gt;. After each BDOS call, it checks whether the DMA address has exceeded the TPA boundary at &lt;code&gt;0xE000&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;E6F5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BDOS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;
&lt;span class="n"&gt;E6F6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;H&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DMA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;high&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;byte&lt;/span&gt;
&lt;span class="n"&gt;E6F7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SUB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;compare&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;against&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;E6F8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;H&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SBC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;below&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;E6F9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SBC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="n"&gt;TPA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;boundary&lt;/span&gt;
&lt;span class="n"&gt;E6FB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;JP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;NC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;E771&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;past&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TPA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;loading&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;SBC A,D&lt;/code&gt; instruction at &lt;code&gt;E6F9&lt;/code&gt; (opcode &lt;code&gt;0x9A&lt;/code&gt;) subtracts the D register and the carry flag from A. This is a 16-bit comparison implemented as a high-byte subtract-with-borrow after the low-byte subtract at &lt;code&gt;E6F7&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The problem: &lt;strong&gt;opcode &lt;code&gt;0x9A&lt;/code&gt; was not in the shadow register tracking.&lt;/strong&gt; The switch statement had &lt;code&gt;SBC A,A&lt;/code&gt; (0x9F), &lt;code&gt;SBC A,B&lt;/code&gt; (0x98), and &lt;code&gt;SBC A,C&lt;/code&gt; (0x99), but not &lt;code&gt;SBC A,D&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Without tracking, the &lt;code&gt;SBC A,D&lt;/code&gt; instruction didn't update &lt;code&gt;shadowF&lt;/code&gt;. The carry flag in the shadow still reflected the preceding &lt;code&gt;SUB E&lt;/code&gt; instruction, which had set carry=0 (no borrow, since 0x80 - 0x00 = 0x80). But the real Z80 computed &lt;code&gt;SBC A,D&lt;/code&gt; with the actual register values and got carry=1 (borrow). When the &lt;code&gt;JP NC,E771&lt;/code&gt; branch came, our shadow said NC=true (carry clear, branch taken) while the Z80 said NC=false (carry set, branch not taken).&lt;/p&gt;
&lt;p&gt;SoftPC jumped to the "Bad load" error handler. The real Z80 continued the load loop. From that point on, softPC and the Z80's actual program counter were desynchronized; every subsequent opcode decode was wrong, and the system was effectively running blind.&lt;/p&gt;
&lt;h4&gt;The Fix&lt;/h4&gt;
&lt;p&gt;Add the missing instructions. All of them:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;// SBC A,r — subtract with carry&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x98&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;uint16_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shadowA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shadowB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shadowF&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;FLAG_C&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="n"&gt;shadowF&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flagsSub8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shadowA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shadowB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="n"&gt;shadowA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0xFF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x99&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cm"&gt;/* SBC A,C */&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x9A&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cm"&gt;/* SBC A,D */&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x9B&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cm"&gt;/* SBC A,E */&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x9C&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cm"&gt;/* SBC A,H */&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x9D&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cm"&gt;/* SBC A,L */&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x9E&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cm"&gt;/* SBC A,(HL) */&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ADC A,r — add with carry (same gap)&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x8A&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cm"&gt;/* ADC A,D */&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x8B&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cm"&gt;/* ADC A,E */&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x8C&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cm"&gt;/* ADC A,H */&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x8D&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cm"&gt;/* ADC A,L */&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x8E&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cm"&gt;/* ADC A,(HL) */&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After this fix, &lt;code&gt;ZORK1.COM&lt;/code&gt; loaded all 68 sectors successfully: 8,704 bytes from DMA address &lt;code&gt;0x0100&lt;/code&gt; to &lt;code&gt;0x2300&lt;/code&gt;, with every BDOS read returning success.&lt;/p&gt;
&lt;h3&gt;Zork Starts, Barely&lt;/h3&gt;
&lt;p&gt;With the load fixed, &lt;a href="https://baud.rs/UdOkDt"&gt;Zork&lt;/a&gt; launched. It read its &lt;code&gt;.DAT&lt;/code&gt; file from disk. The copyright text appeared:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;ZORK&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;I:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;The&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Great&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Underground&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Empire&lt;/span&gt;
&lt;span class="n"&gt;Copyright&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;1981&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;1982&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;1983&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Infocom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Inc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;All&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rights&lt;/span&gt;
&lt;span class="n"&gt;reserved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;ZORK&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;registered&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;trademark&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Infocom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Inc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;Revision&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;88&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Serial&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;840726&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And then... nothing. Or rather, something, but at glacial speed. At approximately 18,000 Z80 clock cycles per second, the text took minutes to render. The game was technically running but practically frozen. Typing a command and waiting for a response meant staring at a blank terminal for an eternity.&lt;/p&gt;
&lt;p&gt;On a 480MHz Cortex-M7, 18,000 Z80 cycles per second means the Arduino was spending roughly &lt;strong&gt;26,000 of its own CPU cycles on every single Z80 clock tick&lt;/strong&gt;. Something was catastrophically wrong with the hot loop.&lt;/p&gt;
&lt;h3&gt;The Performance Crisis&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/giga-shield-assembled-overhead.jpeg" alt="Overhead view of the full hardware stack: the Giga's blue board seated on the red level converter shield, with the RetroShield Z80 extending to the right, USB cable connected" style="float: right; width: 50%; max-width: 460px; margin: 0 0 1em 1.5em; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);"&gt;&lt;/p&gt;
&lt;p&gt;I added a performance counter that measured actual Z80 cycles per second. The numbers were dire: 9,000–18,000 cycles/sec depending on what the Z80 was doing. A real Z80 runs at 2.5–8 MHz. We were three orders of magnitude too slow.&lt;/p&gt;
&lt;p&gt;Five bottlenecks were hiding in the hot loop, each one multiplying the others.&lt;/p&gt;
&lt;h4&gt;Bottleneck 1: A Two-Millisecond Nap on Every Tick&lt;/h4&gt;
&lt;p&gt;Every clock tick included &lt;code&gt;delayMicroseconds(2)&lt;/code&gt;, a 2,000-nanosecond delay to let signals settle through the TXB0108 after toggling the clock. The TXB0108's actual propagation delay is about 4–12 nanoseconds. This was a 200x safety margin I'd added early in debugging and never removed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Replace with 24 inline NOP instructions. At 480MHz, each NOP is ~2ns, giving roughly 50ns of settle time, still 4x more than the TXB0108 needs, but 40x faster than the delay.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kr"&gt;inline&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;__attribute__&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;always_inline&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;busSettle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kr"&gt;__asm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;volatile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"nop&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;nop&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;nop&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;nop&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;nop&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;nop&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;nop&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;nop&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
&lt;span class="w"&gt;                   &lt;/span&gt;&lt;span class="s"&gt;"nop&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;nop&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;nop&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;nop&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;nop&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;nop&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;nop&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;nop&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
&lt;span class="w"&gt;                   &lt;/span&gt;&lt;span class="s"&gt;"nop&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;nop&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;nop&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;nop&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;nop&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;nop&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;nop&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;nop&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Bottleneck 2: Flipping 8 Pins on Every Single Tick&lt;/h4&gt;
&lt;p&gt;This was the real killer. At the end of every &lt;code&gt;cpu_tick()&lt;/code&gt; call, &lt;code&gt;setDataBusInput()&lt;/code&gt; was called to tri-state the data bus pins, switching all 8 data lines from output to input mode. Then at the start of the next memory read, &lt;code&gt;setDataBusOutput()&lt;/code&gt; switched them all back. Each direction change went through the Arduino HAL &lt;code&gt;pinMode()&lt;/code&gt; function 8 times.&lt;/p&gt;
&lt;p&gt;On the STM32H747 with the &lt;a href="https://baud.rs/arduino-mbed"&gt;mbed-based Arduino core&lt;/a&gt;, each &lt;code&gt;pinMode()&lt;/code&gt; call involves HAL abstraction layers, pin table lookups, and clock configuration checks. Eight calls took approximately 16–32 microseconds. This was happening on &lt;em&gt;every single clock tick&lt;/em&gt;, both directions, 16 &lt;code&gt;pinMode()&lt;/code&gt; calls per tick.&lt;/p&gt;
&lt;p&gt;The irony: this direction switching was completely unnecessary. Since all Z80 bus writes are suppressed (pre-writes handle memory stores in software), the data bus never needs to read anything from the Z80 during normal operation. The bus can stay in output mode permanently.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Remove the per-tick &lt;code&gt;setDataBusInput()&lt;/code&gt; call entirely. For the rare cases where direction changes are still needed (certain IO operations), replace &lt;code&gt;pinMode()&lt;/code&gt; with direct GPIO MODER register writes:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#define GPIO_SET_OUTPUT(port, pin) \&lt;/span&gt;
&lt;span class="cp"&gt;    ((port)-&amp;gt;MODER = ((port)-&amp;gt;MODER &amp;amp; ~(3U &amp;lt;&amp;lt; ((pin)*2))) \&lt;/span&gt;
&lt;span class="cp"&gt;                     | (1U &amp;lt;&amp;lt; ((pin)*2)))&lt;/span&gt;
&lt;span class="cp"&gt;#define GPIO_SET_INPUT(port, pin) \&lt;/span&gt;
&lt;span class="cp"&gt;    ((port)-&amp;gt;MODER = ((port)-&amp;gt;MODER &amp;amp; ~(3U &amp;lt;&amp;lt; ((pin)*2))))&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;One register write per pin instead of a full HAL function call.&lt;/p&gt;
&lt;h4&gt;Bottleneck 3: Arduino HAL in the Hot Loop&lt;/h4&gt;
&lt;p&gt;The Arduino &lt;code&gt;digitalRead()&lt;/code&gt; and &lt;code&gt;digitalWrite()&lt;/code&gt; functions are convenient abstractions, but on the STM32H747 they carry significant overhead: pin number lookups, port mapping tables, multiple function calls per operation. The original RetroShield code for the &lt;a href="https://baud.rs/JJg3wB"&gt;Mega 2560&lt;/a&gt; used direct AVR port registers (&lt;code&gt;PORTA&lt;/code&gt;, &lt;code&gt;PORTL&lt;/code&gt;) for fast parallel I/O. On the Giga, the pins are scattered across GPIO ports B, E, G, H, I, J, and K (no single-register solution), but direct register access is still orders of magnitude faster than the HAL.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Map every Arduino pin to its STM32H747 GPIO port and pin number, then replace all hot-path I/O with direct register access.&lt;/p&gt;
&lt;p&gt;The clock signal (toggled every tick) went from ~200ns per call through HAL to ~4ns via the BSRR (Bit Set/Reset Register):&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;// Before&lt;/span&gt;
&lt;span class="cp"&gt;#define CLK_HIGH  digitalWrite(uP_CLK, HIGH)&lt;/span&gt;

&lt;span class="c1"&gt;// After — single atomic register write&lt;/span&gt;
&lt;span class="cp"&gt;#define CLK_HIGH  (GPIOK-&amp;gt;BSRR = (1U &amp;lt;&amp;lt; 2))    &lt;/span&gt;&lt;span class="c1"&gt;// PK2&lt;/span&gt;
&lt;span class="cp"&gt;#define CLK_LOW   (GPIOK-&amp;gt;BSRR = (1U &amp;lt;&amp;lt; 18))   &lt;/span&gt;&lt;span class="c1"&gt;// PK2 reset&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The BSRR register is an elegant STM32 feature: bits [15:0] set outputs high, bits [31:16] set outputs low, and the entire operation is atomic, so no read-modify-write cycle is needed.&lt;/p&gt;
&lt;p&gt;For the address bus (16 pins read every memory cycle), three GPIO IDR (Input Data Register) reads replace sixteen individual &lt;code&gt;digitalRead()&lt;/code&gt; calls:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kr"&gt;inline&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;uint16_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;readAddress&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;jIDR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GPIOJ&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;IDR&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;kIDR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GPIOK&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;IDR&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gIDR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GPIOG&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;IDR&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint16_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jIDR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1U&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;// A0 = PJ12&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gIDR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1U&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;// A1 = PG13&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// ... 14 more bit extractions&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For the data bus (8 pins written every memory read cycle), pins sharing the same GPIO port are combined into a single BSRR write. Port I has three data bus pins, so they get folded into one register operation:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kr"&gt;inline&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;writeDataBus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;byte&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;GPIOE&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;BSRR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x01&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1U&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1U&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;GPIOK&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;BSRR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x02&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1U&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1U&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;GPIOB&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;BSRR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x04&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1U&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1U&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;GPIOH&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;BSRR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x08&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1U&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1U&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Port I: combine 3 data bus pins into one write&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;iBSRR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;iBSRR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1U&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1U&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// PI13&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;iBSRR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1U&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1U&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// PI10&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;iBSRR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x80&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1U&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1U&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// PI15&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;GPIOI&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;BSRR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;iBSRR&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;GPIOG&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;BSRR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1U&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1U&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Bottleneck 4: USB Serial Polling Every Tick&lt;/h4&gt;
&lt;p&gt;The MC6850 ACIA emulation checked &lt;code&gt;Serial.available()&lt;/code&gt; on every clock tick to detect incoming keystrokes. On the Giga, USB CDC serial operations are expensive; each call may involve USB stack processing. At 700K ticks/sec, checking every tick means 700,000 USB stack queries per second.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Check every 256 ticks. That's still a 2,700 Hz polling rate, more than fast enough for interactive typing, and it eliminates 99.6% of the USB overhead.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;clock_cycle_count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0xFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;CONTROL_RTS_STATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Serial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;available&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;reg6850_STATUS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mb"&gt;0b00000001&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;// RDRF set&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CONTROL_RX_INT_ENABLE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;INT_N_LOW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Bottleneck 5: I-Cache Thrashing from Forced Inlining&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;cpu_tick()&lt;/code&gt; function is around 1,200 lines of code, dominated by the shadow register tracking &lt;code&gt;switch&lt;/code&gt; statement with hundreds of cases. It was marked &lt;code&gt;inline __attribute__((always_inline))&lt;/code&gt;, which forces the compiler to inline the entire function body into &lt;code&gt;loop()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The STM32H747's instruction cache is 16KB. Inlining a 1,200-line function creates a binary blob that doesn't fit, causing constant cache misses. Every iteration of the main loop refills the I-cache from flash.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Change to &lt;code&gt;__attribute__((noinline))&lt;/code&gt;. The function call overhead (a few nanoseconds for the branch and return) is negligible compared to the cache thrashing cost. This change also reduced the compiled binary by ~9KB, from 284KB to 276KB.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;__attribute__&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;noinline&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cpu_tick&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// ... 1,200 lines of bus interface and shadow tracking&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;The Result&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Z80 cycles/sec&lt;/td&gt;
&lt;td&gt;~9,000&lt;/td&gt;
&lt;td&gt;~690,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective Z80 clock&lt;/td&gt;
&lt;td&gt;~0.009 MHz&lt;/td&gt;
&lt;td&gt;~0.69 MHz&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Binary size&lt;/td&gt;
&lt;td&gt;284 KB&lt;/td&gt;
&lt;td&gt;276 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time per Z80 tick&lt;/td&gt;
&lt;td&gt;~111 µs&lt;/td&gt;
&lt;td&gt;~1.4 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A &lt;strong&gt;75x speedup&lt;/strong&gt;. The system went from roughly 50,000 Cortex-M7 cycles per Z80 tick down to about 700. Enough for Zork to be fully interactive.&lt;/p&gt;
&lt;h3&gt;Network Reconnection&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/giga-shield-detail-usb.jpeg" alt="Close-up of the USB connection end of the Arduino Giga R1 mounted on the level converter shield, showing the jumper wire connecting 3.3V power between boards" style="float: left; width: 40%; max-width: 380px; margin: 0 1.5em 1em 0; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);"&gt;&lt;/p&gt;
&lt;p&gt;With the performance problem solved, a new issue appeared: the TCP connection to the sector server dropped during long idle periods. Zork is a text adventure; the player types a command, the game responds, and then nothing happens until the next command. During that idle time (which could be minutes while you think about whether to go north or east), the WiFi TCP socket would quietly die. The next disk operation would fail with "Bad Sector."&lt;/p&gt;
&lt;p&gt;The fix was automatic reconnection logic. Before each disk operation, &lt;code&gt;ensureServerConnection()&lt;/code&gt; checks if the TCP socket is still alive. If not, it reconnects to the sector server, re-opens the disk image file that was previously open, and re-seeks to the last position, all transparently, so the Z80 never knows the connection dropped.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;ensureServerConnection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connected&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Serial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[NET] Connection lost, reconnecting..."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SERVER_IP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SERVER_PORT&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;Serial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[NET] Reconnected to server"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;diskFileOpen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;diskActiveFile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;netSendFileCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DISK_CMD_OPEN_RW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;diskActiveFile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;netReadStatus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;diskFileOpen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="c1"&gt;// Re-seek to last known position&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;seekCmd&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;seekCmd&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DISK_CMD_SEEK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;seekCmd&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;diskSeekPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0xFF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;seekCmd&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;diskSeekPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0xFF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;seekCmd&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;diskSeekPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0xFF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seekCmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;netReadStatus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;video controls style="float: right; width: 50%; max-width: 460px; margin: 0 0 1em 1.5em; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);"&gt;
&lt;source src="https://tinycomputers.io/zork-on-retroshield-z80-giga.mp4" type="video/mp4"&gt;
&lt;source src="https://tinycomputers.io/zork-on-retroshield-z80-giga.mov" type="video/quicktime"&gt;
Your browser does not support the video tag.
&lt;/source&gt;&lt;/source&gt;&lt;/video&gt;

&lt;h3&gt;Playing Zork&lt;/h3&gt;
&lt;p&gt;With all the pieces in place (shadow registers covering every instruction CP/M and Zork use, GPIO registers replacing Arduino HAL calls, network reconnection handling idle timeouts), it was time to play.&lt;/p&gt;
&lt;p&gt;Here's a complete boot-to-gameplay session, captured from the serial terminal:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;OK&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;192.168&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;OK&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;192.168&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.248&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;9000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Boot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="n"&gt;OK&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;loaded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;Starting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Z80&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;

&lt;span class="n"&gt;RetroShield&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Z80&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Boot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Loader&lt;/span&gt;
&lt;span class="n"&gt;Copyright&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2025&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Alex&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Jokela&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tinycomputers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;

&lt;span class="n"&gt;Loading&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CPM&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SYS&lt;/span&gt;&lt;span class="o"&gt;.....................................................&lt;/span&gt;
&lt;span class="n"&gt;Boot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;complete&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;

&lt;span class="n"&gt;RetroShield&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.2&lt;/span&gt;
&lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TPA&lt;/span&gt;

&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;zork1&lt;/span&gt;
&lt;span class="n"&gt;ZORK&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;The&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Great&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Underground&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Empire&lt;/span&gt;
&lt;span class="n"&gt;Copyright&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1981&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1982&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1983&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Infocom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Inc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;All&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rights&lt;/span&gt;
&lt;span class="n"&gt;reserved&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;ZORK&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;registered&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;trademark&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Infocom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Inc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;Revision&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;88&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Serial&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;840726&lt;/span&gt;

&lt;span class="n"&gt;West&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;House&lt;/span&gt;
&lt;span class="n"&gt;You&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;are&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;standing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;an&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;west&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;white&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;house&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;
&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;boarded&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;front&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;door&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;There&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;small&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mailbox&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;here&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mailbox&lt;/span&gt;
&lt;span class="n"&gt;Opening&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;small&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mailbox&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reveals&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;leaflet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;take&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;leaflet&lt;/span&gt;
&lt;span class="n"&gt;Taken&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;go&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;south&lt;/span&gt;
&lt;span class="n"&gt;South&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;House&lt;/span&gt;
&lt;span class="n"&gt;You&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;are&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;facing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;south&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;side&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;white&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;house&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;There&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;
&lt;span class="n"&gt;door&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;here&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;windows&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;are&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;boarded&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;go&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;east&lt;/span&gt;
&lt;span class="n"&gt;Behind&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;House&lt;/span&gt;
&lt;span class="n"&gt;You&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;are&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;behind&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;white&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;house&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;leads&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;into&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;forest&lt;/span&gt;
&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;east&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;corner&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;house&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;there&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;small&lt;/span&gt;
&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;which&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;slightly&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ajar&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;
&lt;span class="n"&gt;With&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;great&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;effort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;you&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;far&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;enough&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;allow&lt;/span&gt;
&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;enter&lt;/span&gt;
&lt;span class="n"&gt;Kitchen&lt;/span&gt;
&lt;span class="n"&gt;You&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;are&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;kitchen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;white&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;house&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;seems&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;
&lt;span class="n"&gt;have&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;been&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;used&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;recently&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;preparation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;food&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;passage&lt;/span&gt;
&lt;span class="n"&gt;leads&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;west&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dark&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;staircase&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;can&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;leading&lt;/span&gt;
&lt;span class="n"&gt;upward&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dark&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;chimney&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;leads&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;down&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;east&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;small&lt;/span&gt;
&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;which&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;On&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;an&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;elongated&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;brown&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;smelling&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hot&lt;/span&gt;
&lt;span class="n"&gt;peppers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bottle&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sitting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;The&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;glass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bottle&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;water&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Every command produces the correct response, at interactive speed. The text appears as fast as you'd expect from a terminal session, with no perceptible delay between pressing Enter and seeing the game's response.&lt;/p&gt;
&lt;h3&gt;How It All Fits Together&lt;/h3&gt;
&lt;p&gt;Here's what happens when you type &lt;code&gt;open mailbox&lt;/code&gt; at the Zork prompt, end to end:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Each keystroke arrives over USB serial. Every 256 Z80 clock ticks, the Arduino checks &lt;code&gt;Serial.available()&lt;/code&gt;, finds a character, and sets the MC6850 ACIA status register's RDRF bit.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The Z80 is spinning in the BIOS console input loop, repeatedly executing &lt;code&gt;IN A,(0x80)&lt;/code&gt; to check the ACIA status register. Our shadow register system detects each &lt;code&gt;IN&lt;/code&gt; instruction at M1 time, calls &lt;code&gt;handle_io_read(0x80)&lt;/code&gt;, and drives the status byte onto the data bus during the IO cycle.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;When RDRF is set, the Z80 executes &lt;code&gt;IN A,(0x81)&lt;/code&gt; to read the character. We return the byte from &lt;code&gt;Serial.read()&lt;/code&gt;, and &lt;code&gt;shadowA&lt;/code&gt; gets updated to match.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The BIOS echoes the character by executing &lt;code&gt;OUT (0x81),A&lt;/code&gt;. We detect this at M1 time, use &lt;code&gt;shadowA&lt;/code&gt; for the data value, and call &lt;code&gt;Serial.write()&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;When the user presses Enter, the CCP passes the command to Zork. Zork parses it and starts executing game logic, hundreds of thousands of Z80 instructions manipulating its internal data structures.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;When Zork needs to read from its &lt;code&gt;.DAT&lt;/code&gt; file, the BIOS executes a sequence of &lt;code&gt;OUT&lt;/code&gt; instructions to set up the disk operation: filename characters to port &lt;code&gt;0x13&lt;/code&gt;, seek position to ports &lt;code&gt;0x14&lt;/code&gt;/&lt;code&gt;0x15&lt;/code&gt;/&lt;code&gt;0x19&lt;/code&gt;, DMA address to ports &lt;code&gt;0x16&lt;/code&gt;/&lt;code&gt;0x17&lt;/code&gt;, and a block read command to port &lt;code&gt;0x18&lt;/code&gt;. Each &lt;code&gt;OUT&lt;/code&gt; is intercepted by the shadow register system and forwarded to the sector server over WiFi.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The sector server reads 128 bytes from the disk image file, sends them back over TCP. The Arduino writes them directly into &lt;code&gt;z80RAM&lt;/code&gt; at the DMA address.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Zork processes the data, generates response text, and prints it character by character through the ACIA, each character going through the same &lt;code&gt;OUT (0x81),A&lt;/code&gt; → &lt;code&gt;shadowA&lt;/code&gt; → &lt;code&gt;Serial.write()&lt;/code&gt; path.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Every single one of these operations relies on the shadow register system. The Z80 has no idea the Arduino can't see half its bus signals. It thinks it's talking to normal memory and I/O ports. The Arduino, meanwhile, is running a parallel simulation of the Z80's register state, intercepting every instruction, and making the illusion seamless.&lt;/p&gt;
&lt;h3&gt;The Full Architecture&lt;/h3&gt;
&lt;p&gt;For reference, here's the complete technical stack:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hardware:&lt;/strong&gt;
- Arduino Giga R1 WiFi (STM32H747, 480MHz Cortex-M7, 1MB SRAM, WiFi)
- RetroShield Z80 (real Zilog Z80 CPU, 5V logic)
- Custom level converter PCB (nine TXB0108PW, &lt;a href="https://tinycomputers.io/posts/fiverr-pcb-design-arduino-giga-shield.html"&gt;design details here&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Z80 Memory:&lt;/strong&gt;
- 64KB byte array in Giga's internal SRAM (&lt;code&gt;uint8_t z80RAM[65536]&lt;/code&gt;)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bus Interface (direct STM32 GPIO registers):&lt;/strong&gt;
- Clock: GPIOK pin 2 (BSRR for set/clear)
- Address: 3 IDR reads (GPIOJ, GPIOK, GPIOG) → 16-bit extraction
- Data: 6 BSRR writes (GPIOE, GPIOK, GPIOB, GPIOH, GPIOI combined, GPIOG)
- Control: MREQ via GPIOK pin 7, WR via GPIOE pin 6&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Software Architecture:&lt;/strong&gt;
- Guard-only M1 detection with &lt;code&gt;tStates[256]&lt;/code&gt; timing table
- Software PC tracking (&lt;code&gt;softPC&lt;/code&gt;) for all branch types including conditional
- Shadow registers (A, B, C, D, E, H, L, F, SP) with full ALU flag computation
- Pre-writes for all memory store instructions
- Deferred writes for read-modify-write instructions (INC/DEC (HL), CB prefix on (HL))
- IO handling at M1 time using shadow register values&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Disk I/O:&lt;/strong&gt;
- Rust TCP sector server on local network (192.168.0.248:9000)
- 128-byte CP/M sector transfers over WiFi
- Automatic reconnection with file re-open and seek restore&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Console I/O:&lt;/strong&gt;
- Emulated MC6850 ACIA on ports 0x80/0x81
- &lt;a href="https://baud.rs/xwHWlp"&gt;USB&lt;/a&gt; CDC serial at 115200 baud
- Interrupt-driven receive with throttled polling (every 256 ticks)&lt;/p&gt;
&lt;h3&gt;Pin Mapping Reference&lt;/h3&gt;
&lt;p&gt;For anyone attempting a similar project, here's the complete mapping from Arduino digital pins to STM32H747 GPIO ports. This is essential for the direct register access that makes the performance optimization possible:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;th&gt;Arduino Pin&lt;/th&gt;
&lt;th&gt;STM32 Port/Pin&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CLK&lt;/td&gt;
&lt;td&gt;D52&lt;/td&gt;
&lt;td&gt;PK2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MREQ_N&lt;/td&gt;
&lt;td&gt;D41&lt;/td&gt;
&lt;td&gt;PK7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WR_N&lt;/td&gt;
&lt;td&gt;D40&lt;/td&gt;
&lt;td&gt;PE6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IORQ_N&lt;/td&gt;
&lt;td&gt;D39&lt;/td&gt;
&lt;td&gt;PI14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INT_N&lt;/td&gt;
&lt;td&gt;D50&lt;/td&gt;
&lt;td&gt;PI11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RESET_N&lt;/td&gt;
&lt;td&gt;D38&lt;/td&gt;
&lt;td&gt;PJ7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data bit 0&lt;/td&gt;
&lt;td&gt;D49&lt;/td&gt;
&lt;td&gt;PE4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data bit 1&lt;/td&gt;
&lt;td&gt;D48&lt;/td&gt;
&lt;td&gt;PK0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data bit 2&lt;/td&gt;
&lt;td&gt;D47&lt;/td&gt;
&lt;td&gt;PB2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data bit 3&lt;/td&gt;
&lt;td&gt;D46&lt;/td&gt;
&lt;td&gt;PH15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data bit 4&lt;/td&gt;
&lt;td&gt;D45&lt;/td&gt;
&lt;td&gt;PI13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data bit 5&lt;/td&gt;
&lt;td&gt;D44&lt;/td&gt;
&lt;td&gt;PG10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data bit 6&lt;/td&gt;
&lt;td&gt;D43&lt;/td&gt;
&lt;td&gt;PI10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data bit 7&lt;/td&gt;
&lt;td&gt;D42&lt;/td&gt;
&lt;td&gt;PI15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Addr A0&lt;/td&gt;
&lt;td&gt;D22&lt;/td&gt;
&lt;td&gt;PJ12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Addr A1&lt;/td&gt;
&lt;td&gt;D23&lt;/td&gt;
&lt;td&gt;PG13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Addr A2&lt;/td&gt;
&lt;td&gt;D24&lt;/td&gt;
&lt;td&gt;PG12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Addr A3&lt;/td&gt;
&lt;td&gt;D25&lt;/td&gt;
&lt;td&gt;PJ0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Addr A4&lt;/td&gt;
&lt;td&gt;D26&lt;/td&gt;
&lt;td&gt;PJ14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Addr A5&lt;/td&gt;
&lt;td&gt;D27&lt;/td&gt;
&lt;td&gt;PJ1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Addr A6&lt;/td&gt;
&lt;td&gt;D28&lt;/td&gt;
&lt;td&gt;PJ15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Addr A7&lt;/td&gt;
&lt;td&gt;D29&lt;/td&gt;
&lt;td&gt;PJ2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Addr A8&lt;/td&gt;
&lt;td&gt;D37&lt;/td&gt;
&lt;td&gt;PJ6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Addr A9&lt;/td&gt;
&lt;td&gt;D36&lt;/td&gt;
&lt;td&gt;PK6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Addr A10&lt;/td&gt;
&lt;td&gt;D35&lt;/td&gt;
&lt;td&gt;PJ5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Addr A11&lt;/td&gt;
&lt;td&gt;D34&lt;/td&gt;
&lt;td&gt;PK5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Addr A12&lt;/td&gt;
&lt;td&gt;D33&lt;/td&gt;
&lt;td&gt;PJ4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Addr A13&lt;/td&gt;
&lt;td&gt;D32&lt;/td&gt;
&lt;td&gt;PK4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Addr A14&lt;/td&gt;
&lt;td&gt;D31&lt;/td&gt;
&lt;td&gt;PJ3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Addr A15&lt;/td&gt;
&lt;td&gt;D30&lt;/td&gt;
&lt;td&gt;PK3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The address bus pins are spread across three GPIO ports (J, G, K), so a 16-bit address read requires three IDR register reads and individual bit extraction. Not ideal, but still orders of magnitude faster than sixteen &lt;code&gt;digitalRead()&lt;/code&gt; calls.&lt;/p&gt;
&lt;h3&gt;What's Next&lt;/h3&gt;
&lt;p&gt;The immediate win would be using the Giga's 8MB SDRAM as a disk cache. Download entire disk images over WiFi at boot, then serve all disk I/O from memory. No network latency, no TCP overhead, no reconnection logic needed. CP/M running at SRAM speed on a RAM disk, faster than any physical media the Z80 ever had access to.&lt;/p&gt;
&lt;p&gt;There's also the question of the TXB0108 itself. The level converter PCB works, but three of its nine ICs are essentially decorative; the signals they're supposed to translate (&lt;code&gt;IORQ_N&lt;/code&gt;, &lt;code&gt;RD_N&lt;/code&gt;, and data bus Z80→Arduino during IO) are broken, and the software works around them. A v0.2 of the board replaces 5 of the 9 TXB0108s with purpose-matched ICs that don't rely on auto-direction sensing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;74LVC541&lt;/strong&gt; (U1–U3): Unidirectional buffers for the address bus and control inputs (&lt;code&gt;MREQ_N&lt;/code&gt;, &lt;code&gt;IORQ_N&lt;/code&gt;, &lt;code&gt;RD_N&lt;/code&gt;, &lt;code&gt;WR_N&lt;/code&gt;). VCC at 3.3V with 5V-tolerant inputs. They simply translate 5V→3.3V with no direction ambiguity. This eliminates the stuck-HIGH failures on &lt;code&gt;IORQ_N&lt;/code&gt; and &lt;code&gt;RD_N&lt;/code&gt; entirely.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;74AHCT541&lt;/strong&gt; (U4): Unidirectional buffer for control outputs (&lt;code&gt;CLK&lt;/code&gt;, &lt;code&gt;RESET_N&lt;/code&gt;, &lt;code&gt;INT_N&lt;/code&gt;, &lt;code&gt;NMI_N&lt;/code&gt;). VCC at 5V with TTL-compatible inputs that accept 3.3V drive levels, providing clean 3.3V→5V translation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SN74LVC4245A&lt;/strong&gt; (U5): Bidirectional transceiver for the data bus, with an explicit DIR pin controlled by a Giga GPIO. No more auto-sensing guesswork; the firmware tells the chip which side is driving, so Z80→Arduino data is visible during IO writes for the first time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TXB0108&lt;/strong&gt; (U6–U9): Retained for the remaining 40 channels of pass-through GPIO, where auto-direction sensing works fine.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The firmware payoff is substantial: the entire shadow register architecture (roughly 1,300 lines of opcode tracking, softPC maintenance, pre-writes, and deferred write logic) could be replaced by a single &lt;code&gt;digitalWrite()&lt;/code&gt; to flip the data bus direction pin. That's a lot of complexity removed for one additional GPIO wire.&lt;/p&gt;
&lt;p&gt;But there's something satisfying about the current approach. The shadow register system transforms a passive bus controller into something that understands the Z80's instruction stream at a semantic level. The Arduino doesn't just shuttle bytes; it knows what the Z80 is thinking. And if the goal is to play &lt;a href="https://baud.rs/UdOkDt"&gt;Zork&lt;/a&gt; in the Great Underground Empire on real 1980s hardware controlled by a modern microcontroller over WiFi, well, we're there.&lt;/p&gt;
&lt;p&gt;A note on tooling: this project would have taken considerably longer without &lt;a href="https://baud.rs/claude-code"&gt;Claude Code&lt;/a&gt;. The debugging cycle for a project like this (where you're staring at Z80 opcode tables, cross-referencing flag behavior across hundreds of instructions, and hunting for one wrong carry bit in a 2,600-line Arduino sketch) is brutal. Claude Code served as a tireless pair programmer throughout the process, helping trace through instruction semantics, spotting missing opcodes in the shadow register implementation, working through the GPIO register mappings for the STM32H747, and iterating on performance optimizations. The feedback loop that would normally stretch across days of manual datasheet cross-referencing compressed into hours.&lt;/p&gt;
&lt;div style="clear: both;"&gt;&lt;/div&gt;

&lt;div class="sponsor-widget"&gt;
&lt;div class="sponsor-widget-header"&gt;&lt;a href="https://baud.rs/youwpy"&gt;&lt;img src="https://tinycomputers.io/images/pcbway-logo.png" alt="PCBWay" style="height: 22px; vertical-align: middle; margin-right: 8px;"&gt;&lt;/a&gt; Sponsored Hardware&lt;/div&gt;
&lt;p&gt;This project was made possible by &lt;a href="https://baud.rs/youwpy"&gt;PCBWay&lt;/a&gt;, who sponsored the manufacturing of the custom level converter shield. PCBWay offers PCB prototyping, assembly, CNC machining, and 3D printing services, from one-off prototypes to production runs. Their support covered the fabrication costs for this board, letting me focus on the engineering instead of the budget. If you have a PCB design ready to go, check them out at &lt;a href="https://baud.rs/youwpy"&gt;pcbway.com&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;

&lt;h3&gt;Source Code&lt;/h3&gt;
&lt;p&gt;All source code, firmware, and hardware design files for this project are open source:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://baud.rs/GvjdJK"&gt;retroshield-z80-cpm-giga&lt;/a&gt;&lt;/strong&gt;: Arduino Giga R1 firmware, CP/M system files, and disk image (BSD 3-Clause)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://baud.rs/60cj4a"&gt;retroshield-sector-server&lt;/a&gt;&lt;/strong&gt;: Rust TCP sector server for WiFi-based disk I/O (BSD 3-Clause)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://baud.rs/9s81Mz"&gt;retroshield-level-shifter-pcb&lt;/a&gt;&lt;/strong&gt;: KiCad design files, Gerber files, BOM, and schematic for the level converter shield (CC BY-SA 4.0)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;This is the third post in the Arduino Giga R1 + RetroShield Z80 series:&lt;/em&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://tinycomputers.io/posts/fiverr-pcb-design-arduino-giga-shield.html"&gt;My Experience Using Fiverr for Custom PCB Design: A $468 Arduino Giga Shield&lt;/a&gt;, designing the level converter&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://tinycomputers.io/posts/cpm-on-arduino-giga-r1-wifi.html"&gt;Porting CP/M to the Arduino Giga R1: When Level Converters Fight Back&lt;/a&gt;, the hardware stack, TXB0108 failures, shadow registers, and sector server&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Playing Zork on a Real Z80 (this post), getting CP/M to boot, the "Bad load" bug, 75x performance optimization, and interactive Zork gameplay&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;</description><category>arduino</category><category>arduino giga</category><category>cp/m</category><category>gpio</category><category>hardware</category><category>infocom</category><category>level shifter</category><category>performance</category><category>retro computing</category><category>retroshield</category><category>rust</category><category>sector server</category><category>stm32</category><category>wifi</category><category>z80</category><category>zork</category><guid>https://tinycomputers.io/posts/zork-on-retroshield-z80-arduino-giga.html</guid><pubDate>Sun, 15 Feb 2026 12:00:00 GMT</pubDate></item><item><title>Porting CP/M to the Arduino Giga R1: When Level Converters Fight Back</title><link>https://tinycomputers.io/posts/cpm-on-arduino-giga-r1-wifi.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/cpm-on-arduino-giga-r1-wifi_tts.mp3" type="audio/mpeg"&gt;

&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;18 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;My &lt;a href="https://tinycomputers.io/posts/cpm-on-physical-retroshield-z80.html"&gt;previous CP/M build&lt;/a&gt; runs great. A real Z80 on a &lt;a href="https://baud.rs/87wbBL"&gt;RetroShield&lt;/a&gt;, DRAM shield for 64KB, SD card for disk images, all sitting on an &lt;a href="https://baud.rs/DzXGr4"&gt;Arduino Mega 2560&lt;/a&gt;. It boots CP/M, runs Zork, the works. So naturally I decided to make my life harder.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/giga-level-converter-retroshield.jpeg" alt="The Arduino Giga R1 WiFi (blue) mounted on the custom red level converter PCB, with the RetroShield Z80 partially inserted on the right" style="float: right; width: 55%; max-width: 500px; margin: 0 0 1em 1.5em; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);"&gt;&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://baud.rs/poSQeo"&gt;Arduino Giga R1 WiFi&lt;/a&gt; is a significantly more powerful board: a dual-core STM32H747 running at 480MHz, 1MB of internal SRAM, 8MB of SDRAM, and built-in WiFi. Where the Mega's 16MHz AVR crawled through bus cycles, the Giga could theoretically fly. And all that internal RAM means we can ditch the &lt;a href="https://baud.rs/iJn6Sd"&gt;KDRAM2560&lt;/a&gt; DRAM shield entirely, leaving just a 64KB byte array in SRAM.&lt;/p&gt;
&lt;p&gt;There was just one problem. The Giga runs at 3.3V logic. The Z80 runs at 5V. And as I'd learn the hard way, bridging that gap would consume more debugging hours than everything else combined.&lt;/p&gt;
&lt;p&gt;This post documents the port: the hardware stack, the architectural pivot to WiFi-based disk I/O, the level converter nightmare, the shadow register workaround that saved the project, and the Rust sector server that ties it all together. If you want the backstory on the &lt;a href="https://tinycomputers.io/posts/fiverr-pcb-design-arduino-giga-shield.html"&gt;custom level converter PCB&lt;/a&gt;, designed by &lt;a href="https://baud.rs/tkQg41"&gt;Elijah on Fiverr&lt;/a&gt;, that's a separate post.&lt;/p&gt;
&lt;div class="sponsor-widget"&gt;
&lt;div class="sponsor-widget-header"&gt;&lt;a href="https://baud.rs/youwpy"&gt;&lt;img src="https://tinycomputers.io/images/pcbway-logo.png" alt="PCBWay" style="height: 22px; vertical-align: middle; margin-right: 8px;"&gt;&lt;/a&gt; Sponsored Hardware&lt;/div&gt;
&lt;p&gt;This project was made possible by &lt;a href="https://baud.rs/youwpy"&gt;PCBWay&lt;/a&gt;, who sponsored the manufacturing of the custom level converter shield. PCBWay offers PCB prototyping, assembly, CNC machining, and 3D printing services, from one-off prototypes to production runs. Their support covered the fabrication costs for this board, letting me focus on the engineering instead of the budget. If you have a PCB design ready to go, check them out at &lt;a href="https://baud.rs/youwpy"&gt;pcbway.com&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;

&lt;h3&gt;The Hardware Stack&lt;/h3&gt;
&lt;p&gt;The upgraded system has fewer physical components than the Mega version, but more going on under the hood.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Mega Version&lt;/th&gt;
&lt;th&gt;Giga Version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Processor&lt;/td&gt;
&lt;td&gt;ATmega2560, 16MHz&lt;/td&gt;
&lt;td&gt;STM32H747, 480MHz Cortex-M7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logic Level&lt;/td&gt;
&lt;td&gt;5V native&lt;/td&gt;
&lt;td&gt;3.3V (needs level converter)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Z80 RAM&lt;/td&gt;
&lt;td&gt;KDRAM2560 DRAM shield&lt;/td&gt;
&lt;td&gt;64KB byte array in SRAM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bus I/O&lt;/td&gt;
&lt;td&gt;AVR port registers (parallel)&lt;/td&gt;
&lt;td&gt;digitalRead/Write (per-pin)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disk Storage&lt;/td&gt;
&lt;td&gt;SD card (software SPI)&lt;/td&gt;
&lt;td&gt;WiFi to sector server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extra Hardware&lt;/td&gt;
&lt;td&gt;DRAM shield + SD adapter&lt;/td&gt;
&lt;td&gt;Level converter board only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The RetroShield Z80 plugs in the same way; it uses the same physical pin positions. The level converter board sits between the Giga and the RetroShield, translating all bus signals between 3.3V and 5V. The board uses &lt;a href="https://baud.rs/hY6ydl"&gt;TXB0108&lt;/a&gt; bidirectional level converters, which sense the drive direction automatically.&lt;/p&gt;
&lt;p&gt;At least, that's what they're supposed to do.&lt;/p&gt;
&lt;h4&gt;CP/M Memory Map&lt;/h4&gt;
&lt;p&gt;The Z80 sees the same 64KB address space as on the Mega:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="mf"&gt;0000&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;00&lt;/span&gt;&lt;span class="n"&gt;FF&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;Page&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Zero&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jump&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;vectors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;FCBs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mf"&gt;0100&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;DFFF&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;TPA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Transient&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Program&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Area&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;56&lt;/span&gt;&lt;span class="n"&gt;KB&lt;/span&gt;
&lt;span class="n"&gt;E000&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;E7FF&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;CCP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Processor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;E800&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;F5FF&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;BDOS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Basic&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Disk&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Operating&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;Sys&lt;/span&gt;&lt;span class="n"&gt;tem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;F600&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;FFFF&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;BIOS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Basic&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;O&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;Sys&lt;/span&gt;&lt;span class="n"&gt;tem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;On the Mega, this lived in the KDRAM2560's dynamic RAM with its complex refresh timing. On the Giga, it's just:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;byte&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;z80RAM&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;One line. The Giga's 1MB of internal SRAM makes the entire DRAM shield unnecessary.&lt;/p&gt;
&lt;h3&gt;WiFi Instead of SD&lt;/h3&gt;
&lt;p&gt;The original plan was to keep the SD card. The Mega version used software SPI on pins 4-7 since the RetroShield claims the hardware SPI pins. On the Giga, the RetroShield still claims all 76 digital pins, but this time there are no spare analog pins conveniently routed for software SPI either.&lt;/p&gt;
&lt;p&gt;The initial idea was to wire a MicroSD adapter to the analog pins. But that meant more custom wiring on top of the already-custom level converter board. And anyone trying to replicate this project would need to solder yet another adapter.&lt;/p&gt;
&lt;p&gt;Then it hit me: the Giga has WiFi built in. Why not serve disk images over the network?&lt;/p&gt;
&lt;p&gt;The more I thought about it, the more sense it made:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No additional hardware.&lt;/strong&gt; WiFi is built into the Giga. Zero extra wiring.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Better reproducibility.&lt;/strong&gt; The project already requires a custom level converter. Adding another custom wiring job makes it harder for others to build.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The sector server is just software.&lt;/strong&gt; Anyone can download and run a binary. Compare that to soldering an SD adapter to analog pins.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It plays to the Giga's strengths.&lt;/strong&gt; If you're upgrading from a Mega, you might as well use what makes the Giga special: WiFi and RAM.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Future potential.&lt;/strong&gt; The 8MB SDRAM could cache entire disk images downloaded over WiFi at boot. CP/M on a RAM disk, faster than any physical media the Z80 ever had access to.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The tradeoff is that the system is no longer self-contained. It needs a WiFi network and a computer running the sector server. For a project that already requires a custom level converter PCB, this felt acceptable.&lt;/p&gt;
&lt;h3&gt;The Level Converter Problem&lt;/h3&gt;
&lt;p&gt;This is where the project nearly died.&lt;/p&gt;
&lt;p&gt;The TXB0108 is a popular bidirectional level converter. It uses auto-direction sensing: whichever side drives a signal stronger "wins," and the converter translates accordingly. This works well for simple I2C and SPI signals where direction is clear.&lt;/p&gt;
&lt;p&gt;It does not work well for a Z80 bus.&lt;/p&gt;
&lt;h4&gt;Signal-by-Signal Breakdown&lt;/h4&gt;
&lt;p&gt;I built a pin diagnostic sketch to test each signal through the level converter. Here's what I found:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Pin&lt;/th&gt;
&lt;th&gt;Expected&lt;/th&gt;
&lt;th&gt;Actual&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MREQ_N&lt;/td&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;td&gt;Toggles on memory access&lt;/td&gt;
&lt;td&gt;Toggles correctly&lt;/td&gt;
&lt;td&gt;Working&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WR_N&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;Toggles on writes&lt;/td&gt;
&lt;td&gt;Memory writes only&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RD_N&lt;/td&gt;
&lt;td&gt;53&lt;/td&gt;
&lt;td&gt;Toggles on reads&lt;/td&gt;
&lt;td&gt;Stuck HIGH always&lt;/td&gt;
&lt;td&gt;Broken&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IORQ_N&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;td&gt;Toggles on I/O access&lt;/td&gt;
&lt;td&gt;Stuck HIGH always&lt;/td&gt;
&lt;td&gt;Broken&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Address Bus&lt;/td&gt;
&lt;td&gt;22-37&lt;/td&gt;
&lt;td&gt;16-bit address&lt;/td&gt;
&lt;td&gt;All bits correct&lt;/td&gt;
&lt;td&gt;Working&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data Bus (A→Z80)&lt;/td&gt;
&lt;td&gt;42-49&lt;/td&gt;
&lt;td&gt;Arduino drives data&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;td&gt;Working&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data Bus (Z80→A)&lt;/td&gt;
&lt;td&gt;42-49&lt;/td&gt;
&lt;td&gt;Z80 drives data&lt;/td&gt;
&lt;td&gt;Memory writes only&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Two signals completely stuck. Two signals working only half the time. The auto-direction sensing that makes the TXB0108 convenient is exactly what makes it unreliable here; the Z80's bus signals have complex timing relationships where drive strength varies throughout the cycle.&lt;/p&gt;
&lt;h4&gt;RD_N: Permanently Stuck&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;RD_N&lt;/code&gt; on pin 53 never toggles. It reads HIGH regardless of what the Z80 is doing. Pin 53 is the hardware SPI SCK pin on the Mega, and may have internal pull-ups or other conflicts on the Giga's STM32. Combined with the TXB0108's direction sensing, the signal simply can't get through.&lt;/p&gt;
&lt;p&gt;The fix is trivial once you realize it: during any bus cycle, the Z80 is either reading or writing. They're mutually exclusive. So:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#define STATE_RD_N (!STATE_WR_N)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If &lt;code&gt;WR_N&lt;/code&gt; isn't asserted, it must be a read. This works for all standard Z80 bus operations.&lt;/p&gt;
&lt;h4&gt;IORQ_N: The Big One&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;IORQ_N&lt;/code&gt; on pin 39 is also stuck HIGH. This is the signal that tells us the Z80 wants to talk to a peripheral: console I/O, disk I/O, everything. Without it, we have no way to detect I/O operations through the bus.&lt;/p&gt;
&lt;p&gt;But we have something the bus doesn't know about: we control the Z80's memory. Every byte the Z80 fetches comes from &lt;code&gt;z80RAM[]&lt;/code&gt;, which we serve. We can read the opcode stream and know exactly what instruction the Z80 is executing, including &lt;code&gt;OUT (n), A&lt;/code&gt; (opcode &lt;code&gt;0xD3&lt;/code&gt;) and &lt;code&gt;IN A, (n)&lt;/code&gt; (opcode &lt;code&gt;0xDB&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;So instead of watching for IORQ_N to go low, we watch for the Z80 to fetch an I/O instruction from memory.&lt;/p&gt;
&lt;h4&gt;Data Bus: Invisible During I/O OUT&lt;/h4&gt;
&lt;p&gt;This was the subtlest failure. During &lt;code&gt;OUT (n), A&lt;/code&gt;, the Z80 puts the A register value on the data bus. We should be able to read it. But we can't.&lt;/p&gt;
&lt;p&gt;The TXB0108 latches the last strongly-driven value. Since the Arduino drives the data bus during memory reads (pushing 3.3V through the converter to the Z80's 5V side), the converter's direction gets stuck. When the Z80 tries to drive data back during an I/O write, its 5V output can't overcome the converter's latched direction.&lt;/p&gt;
&lt;p&gt;I confirmed this by sampling the data bus at every clock tick during an &lt;code&gt;OUT&lt;/code&gt; cycle. It showed &lt;code&gt;0x80&lt;/code&gt; (the last value the Arduino had driven, a port number) at every single tick. Zero variation. The Z80's output was completely invisible.&lt;/p&gt;
&lt;h4&gt;WR_N: Only Works Sometimes&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;WR_N&lt;/code&gt; toggles correctly during memory write cycles but never during I/O write cycles. The timing or drive strength during I/O is just different enough that the converter can't track it.&lt;/p&gt;
&lt;p&gt;This meant we couldn't use WR_N to detect when an I/O write was complete either. Every signal we'd normally use for I/O detection was either stuck or unreliable.&lt;/p&gt;
&lt;h3&gt;Shadow Register Tracking&lt;/h3&gt;
&lt;p&gt;The solution to the data bus problem is to never read the data bus during I/O at all. Instead, we maintain a shadow copy of the Z80's A register by watching the opcode stream.&lt;/p&gt;
&lt;p&gt;Since we serve every byte the Z80 reads from &lt;code&gt;z80RAM[]&lt;/code&gt;, we can decode the instruction stream in real time. When we see &lt;code&gt;LD A, 0x03&lt;/code&gt; (opcode &lt;code&gt;0x3E 0x03&lt;/code&gt;), we set &lt;code&gt;shadowA = 0x03&lt;/code&gt;. When we later see &lt;code&gt;OUT (0x10), A&lt;/code&gt; (opcode &lt;code&gt;0xD3 0x10&lt;/code&gt;), we already know A contains &lt;code&gt;0x03&lt;/code&gt;, so there's no need to read the bus.&lt;/p&gt;
&lt;h4&gt;M1 Detection&lt;/h4&gt;
&lt;p&gt;The first challenge is knowing when the Z80 is fetching an opcode (M1 cycle) versus reading data. We detect M1 by watching for the first MREQ-active read after a MREQ-inactive cycle, the rising-to-falling edge of MREQ activity:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mreq_active&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;digitalRead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uP_MREQ_N&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mreq_active&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;prevMREQ_active&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;opcodeSkip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;opcode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;z80RAM&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uP_ADDR&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// This is an M1 fetch — decode the instruction&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;The opcodeSkip Counter&lt;/h4&gt;
&lt;p&gt;After M1, the Z80 runs a refresh cycle (which reuses the address bus; when the I register is 0 after reset, refresh addresses overlap with boot code at 0x0000+). If we mistook a refresh cycle for another M1, we'd corrupt the shadow registers by "decoding" whatever data happened to be at the refresh address.&lt;/p&gt;
&lt;p&gt;The fix is a 256-entry lookup table that tells us how many MREQ-active read cycles to skip after each opcode:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;skipCount&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// 0x00-0x0F: NOP=1, LD BC,nn=3, ...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// ... 256 entries covering every Z80 opcode&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;A 1-byte instruction like &lt;code&gt;NOP&lt;/code&gt; gets skip=1 (refresh only). A 3-byte instruction like &lt;code&gt;LD HL, nn&lt;/code&gt; gets skip=3 (refresh + 2 operand reads). I/O instructions get skip=0 because their skipping is handled by the I/O state machine. After each M1, we set &lt;code&gt;opcodeSkip = skipCount[opcode]&lt;/code&gt; and decrement it on each subsequent MREQ-active read cycle.&lt;/p&gt;
&lt;h4&gt;Register Tracking&lt;/h4&gt;
&lt;p&gt;We don't need to track every Z80 register, just enough to know what value A holds when an &lt;code&gt;OUT&lt;/code&gt; happens. The BIOS uses a relatively small set of instructions to load registers before I/O:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;switch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opcode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// === IO Instructions ===&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0xD3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// OUT (n), A&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;z80RAM&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uP_ADDR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;handle_io_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shadowA&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0xDB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// IN A, (n)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;z80RAM&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uP_ADDR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;ioResponse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;handle_io_read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;shadowA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ioResponse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// IN updates A&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// === A register tracking ===&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x3E&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shadowA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;z80RAM&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uP_ADDR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// LD A, n&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0xAF&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shadowA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;                     &lt;/span&gt;&lt;span class="c1"&gt;// XOR A&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x79&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shadowA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shadowC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="c1"&gt;// LD A, C&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x78&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shadowA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shadowB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="c1"&gt;// LD A, B&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x7C&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shadowA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shadowH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="c1"&gt;// LD A, H&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x7D&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shadowA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shadowL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="c1"&gt;// LD A, L&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x7E&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shadowA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;z80RAM&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;shadowH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shadowL&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0xE6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shadowA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;z80RAM&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uP_ADDR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// AND n&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0xF6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shadowA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;z80RAM&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uP_ADDR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// OR n&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x2F&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shadowA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;shadowA&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="c1"&gt;// CPL&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x3C&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shadowA&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;                         &lt;/span&gt;&lt;span class="c1"&gt;// INC A&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x3D&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shadowA&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;                         &lt;/span&gt;&lt;span class="c1"&gt;// DEC A&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// === B, C, H, L tracking (needed because A loads from them) ===&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x06&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shadowB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;z80RAM&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uP_ADDR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// LD B, n&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x0E&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shadowC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;z80RAM&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uP_ADDR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// LD C, n&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// LD HL, nn&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;shadowL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;z80RAM&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uP_ADDR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;shadowH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;z80RAM&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uP_ADDR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// ... plus INC/DEC HL, LD between registers, etc.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This isn't a full Z80 emulator; it's just enough to track register flow from loads to I/O instructions. If the BIOS uses an instruction we don't track, shadowA will be wrong and the I/O operation will get bad data. But the CP/M BIOS is a known codebase, so we can enumerate exactly which instructions it uses and make sure they're covered.&lt;/p&gt;
&lt;h4&gt;IO State Machine&lt;/h4&gt;
&lt;p&gt;For &lt;code&gt;OUT&lt;/code&gt;, we handle the write immediately when we detect &lt;code&gt;0xD3&lt;/code&gt; at M1, since we already know the port (from RAM) and the data (from shadowA). Then we let the skip counter consume the remaining machine cycles.&lt;/p&gt;
&lt;p&gt;For &lt;code&gt;IN&lt;/code&gt;, it's trickier because the Z80 needs to actually read our response off the data bus. We use a small state machine:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;IO_IDLE&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;detect&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;xDB&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;IO_IN_PENDING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="n"&gt;handle_io_read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;IO_IN_PENDING&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;opcodeSkip&lt;/span&gt; &lt;span class="n"&gt;reaches&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;IO_IN_DRIVING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;drive&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="n"&gt;bus&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;IO_IN_DRIVING&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt; &lt;span class="n"&gt;MREQ&lt;/span&gt; &lt;span class="n"&gt;goes&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;IO_IDLE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;release&lt;/span&gt; &lt;span class="n"&gt;bus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resume&lt;/span&gt; &lt;span class="n"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;During &lt;code&gt;IO_IN_DRIVING&lt;/code&gt;, we keep the response byte on the data bus and ignore other processing until the Z80's next M1 fetch (signaled by MREQ going active again).&lt;/p&gt;
&lt;h3&gt;The Sector Server&lt;/h3&gt;
&lt;p&gt;With the Arduino side handling bus-level I/O through shadow registers, the disk I/O ports translate to network messages. The Z80 BIOS writes a filename character-by-character to port &lt;code&gt;0x13&lt;/code&gt;, writes seek bytes to ports &lt;code&gt;0x14&lt;/code&gt;/&lt;code&gt;0x15&lt;/code&gt;/&lt;code&gt;0x19&lt;/code&gt;, sets the DMA address via ports &lt;code&gt;0x16&lt;/code&gt;/&lt;code&gt;0x17&lt;/code&gt;, then triggers a block read/write on port &lt;code&gt;0x18&lt;/code&gt;. The Arduino accumulates this state, then forwards the operation to the sector server over TCP.&lt;/p&gt;
&lt;h4&gt;Protocol&lt;/h4&gt;
&lt;p&gt;The server speaks a simple binary protocol that mirrors the BIOS port commands:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Byte&lt;/th&gt;
&lt;th&gt;Payload&lt;/th&gt;
&lt;th&gt;Response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OPEN_READ&lt;/td&gt;
&lt;td&gt;0x01&lt;/td&gt;
&lt;td&gt;filename\0&lt;/td&gt;
&lt;td&gt;status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CREATE&lt;/td&gt;
&lt;td&gt;0x02&lt;/td&gt;
&lt;td&gt;filename\0&lt;/td&gt;
&lt;td&gt;status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OPEN_APPEND&lt;/td&gt;
&lt;td&gt;0x03&lt;/td&gt;
&lt;td&gt;filename\0&lt;/td&gt;
&lt;td&gt;status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEEK_START&lt;/td&gt;
&lt;td&gt;0x04&lt;/td&gt;
&lt;td&gt;(none)&lt;/td&gt;
&lt;td&gt;status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLOSE&lt;/td&gt;
&lt;td&gt;0x05&lt;/td&gt;
&lt;td&gt;(none)&lt;/td&gt;
&lt;td&gt;status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DIR&lt;/td&gt;
&lt;td&gt;0x06&lt;/td&gt;
&lt;td&gt;(none)&lt;/td&gt;
&lt;td&gt;status + listing\0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OPEN_RW&lt;/td&gt;
&lt;td&gt;0x07&lt;/td&gt;
&lt;td&gt;filename\0&lt;/td&gt;
&lt;td&gt;status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEEK&lt;/td&gt;
&lt;td&gt;0x08&lt;/td&gt;
&lt;td&gt;3 bytes LE offset&lt;/td&gt;
&lt;td&gt;status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;READ_BLOCK&lt;/td&gt;
&lt;td&gt;0x10&lt;/td&gt;
&lt;td&gt;(none)&lt;/td&gt;
&lt;td&gt;status + 128 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WRITE_BLOCK&lt;/td&gt;
&lt;td&gt;0x11&lt;/td&gt;
&lt;td&gt;128 bytes&lt;/td&gt;
&lt;td&gt;status&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Status is a single byte: &lt;code&gt;0x00&lt;/code&gt; for OK, &lt;code&gt;0x01&lt;/code&gt; for error. Block size is 128 bytes, a CP/M sector.&lt;/p&gt;
&lt;h4&gt;Implementation&lt;/h4&gt;
&lt;p&gt;The server is written in Rust with minimal dependencies (just &lt;code&gt;socket2&lt;/code&gt; for &lt;code&gt;SO_REUSEADDR&lt;/code&gt;). It started single-threaded, which worked fine until the Giga crashed and rebooted. The old TCP connection would hang in the server's blocking &lt;code&gt;read_exact()&lt;/code&gt;, and the Giga's new connection attempt would queue indefinitely. Classic deadlock.&lt;/p&gt;
&lt;p&gt;The fix was threaded connections with timeouts:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;incoming&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;base_dir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;base_dir&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;move&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;handle_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;base_dir&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;eprintln!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[!] Accept error: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Each client gets its own thread. The accept loop never blocks. Read timeouts (30s mid-command, 300s idle) automatically drop dead connections. &lt;code&gt;SO_REUSEADDR&lt;/code&gt; lets the server restart instantly without port conflicts.&lt;/p&gt;
&lt;p&gt;The server also sanitizes filenames (rejecting path traversal and special characters) and tracks session metrics:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gh"&gt;Session Summary&lt;/span&gt;
&lt;span class="gh"&gt;---------------&lt;/span&gt;
Duration:        00:00:00
Commands:        11
Files opened:    2
Seeks:           1
Sectors read:    5
Sectors written: 1
Bytes read:      640 (640 B)
Bytes written:   128 (128 B)
Errors:          0
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Running It&lt;/h4&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# Build&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sector_server&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cargo&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;--release

&lt;span class="c1"&gt;# Serve CP/M files on port 9000&lt;/span&gt;
./sector_server/target/release/sector_server&lt;span class="w"&gt; &lt;/span&gt;./kz80_cpm&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;9000&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The directory needs &lt;code&gt;boot.bin&lt;/code&gt;, &lt;code&gt;CPM.SYS&lt;/code&gt;, and the disk images (&lt;code&gt;A.DSK&lt;/code&gt;, &lt;code&gt;B.DSK&lt;/code&gt;, etc.).&lt;/p&gt;
&lt;h3&gt;Proof of Concept&lt;/h3&gt;
&lt;p&gt;Before wiring up the full shadow register machinery, I wrote a minimal POC sketch that tests just the WiFi + sector server communication. It connects, opens files, reads blocks, seeks, and closes, verifying the network layer end-to-end without any Z80 involvement.&lt;/p&gt;
&lt;p&gt;All 8 tests passed:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;=== Sector Server POC ===

WiFi: connecting to TP-Link_A8A8 ... OK (192.168.0.75)
Server: connecting to 192.168.0.248:9000 ... OK

--- Test 1: OPEN_READ boot.bin ---
  Status: OK
--- Test 2: READ_BLOCK (128 bytes) ---
  Status: OK
  Data:
F3 31 00 04 3E 03 D3 80 3E 15 D3 80 21 83 00 CD
7B 00 21 43 01 CD 70 00 3E 01 D3 10 DB 11 E6 02
--- Test 3: READ_BLOCK (next 128 bytes) ---
  Status: OK
  Data:
23 18 F8 0D 0A 52 65 74 72 6F 53 68 69 65 6C 64
20 5A 38 30 20 42 6F 6F 74 20 4C 6F 61 64 65 72
--- Test 4: CLOSE ---
  Status: OK
--- Test 5: OPEN_RW A.DSK ---
  Status: OK
--- Test 6: SEEK offset=6656 ---
  Status: OK
--- Test 7: READ_BLOCK (directory sector) ---
  Status: OK
  Data:
00 5A 4F 52 4B 31 20 20 20 43 4F 4D 00 00 00 44
--- Test 8: CLOSE ---
  Status: OK

=== All tests complete ===
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Some things to note: the first bytes of &lt;code&gt;boot.bin&lt;/code&gt; are &lt;code&gt;F3 31 00 04&lt;/code&gt;, which disassembles to &lt;code&gt;DI; LD SP, 0x0400&lt;/code&gt;, the boot loader's first instructions, correct. The second block contains the ASCII string "RetroShield Z80 Boot Loader." And the directory sector from &lt;code&gt;A.DSK&lt;/code&gt; shows &lt;code&gt;ZORK1   COM&lt;/code&gt;. Zork is on the disk, waiting.&lt;/p&gt;
&lt;h3&gt;Boot Sequence&lt;/h3&gt;
&lt;p&gt;When the full sketch runs, the expected boot sequence is:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="o"&gt;======================================&lt;/span&gt;
&lt;span class="n"&gt;RetroShield&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Z80&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.2&lt;/span&gt;
&lt;span class="n"&gt;Arduino&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Giga&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;R1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;WiFi&lt;/span&gt;
&lt;span class="o"&gt;======================================&lt;/span&gt;

&lt;span class="n"&gt;Z80&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RAM&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;OK&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="n"&gt;KB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SRAM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;WiFi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="n"&gt;OK&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;192.168&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;OK&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;192.168&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.248&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;9000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Boot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="n"&gt;OK&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;384&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;loaded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;Starting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Z80&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After "Starting Z80...", the Z80 boot loader runs. It opens &lt;code&gt;CPM.SYS&lt;/code&gt; via the sector server, loads the CCP and BDOS into memory at &lt;code&gt;0xE000&lt;/code&gt;, jumps to the BIOS cold boot at &lt;code&gt;0xF600&lt;/code&gt;, and if everything works, prints a banner and the &lt;code&gt;A&amp;gt;&lt;/code&gt; prompt.&lt;/p&gt;
&lt;h3&gt;What's Next&lt;/h3&gt;
&lt;p&gt;The WiFi communication works. The shadow register tracking mechanism works; I've confirmed partial serial output from the Z80 (the ACIA console I/O goes through the same shadow register path). The instruction skip counter prevents refresh cycle confusion. All the pieces are in place.&lt;/p&gt;
&lt;p&gt;What remains is completeness testing. The shadow register tracking needs to cover every instruction the BIOS and CCP actually use. If the Z80 executes an instruction that modifies A through a path we don't track (say, a &lt;code&gt;POP AF&lt;/code&gt; or &lt;code&gt;EX AF, AF'&lt;/code&gt;), the shadow will be wrong and the next &lt;code&gt;OUT&lt;/code&gt; will send garbage. The fix is straightforward (add more cases to the switch statement) but requires methodical testing.&lt;/p&gt;
&lt;p&gt;There's also the 8MB SDRAM sitting unused on the Giga. Once CP/M boots reliably, the obvious next step is downloading entire disk images into SDRAM over WiFi at startup. At that point, all disk I/O becomes memory-mapped, with no network latency and no TCP overhead. CP/M running at memory speed on a RAM disk, served from a real Z80 that thinks it's talking to a floppy drive.&lt;/p&gt;
&lt;p&gt;Once the project is stable and CP/M boots reliably, I plan to open-source the full KiCad PCB design files for the level converter shield, along with the Arduino sketch and sector server. The level converter board uses nine TXB0108PW ICs in TSSOP-20 packages to translate all 72 signals between the Giga's 3.3V and the RetroShield's 5V. It's a straightforward two-layer design that anyone could get fabricated.&lt;/p&gt;
&lt;p&gt;The debugging journey from "IORQ_N is stuck" to "let's just decode the entire instruction stream in software" was not the path I expected to take. But it turned a level converter limitation into something arguably more interesting: a system where the Arduino doesn't just babysit the Z80's bus signals, but understands what the Z80 is thinking.&lt;/p&gt;</description><category>arduino</category><category>arduino giga</category><category>cp/m</category><category>hardware</category><category>level shifter</category><category>retro computing</category><category>retroshield</category><category>rust</category><category>sector server</category><category>stm32</category><category>wifi</category><category>z80</category><guid>https://tinycomputers.io/posts/cpm-on-arduino-giga-r1-wifi.html</guid><pubDate>Sat, 14 Feb 2026 21:00:00 GMT</pubDate></item><item><title>The Z-80 Microcomputer Handbook: A 1978 Reference That Outlived Its Era</title><link>https://tinycomputers.io/posts/the-z80-microcomputer-handbook-william-barden.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/the-z80-microcomputer-handbook-william-barden_tts.mp3" type="audio/mpeg"&gt;
Your browser does not support the audio element.
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;26 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;&lt;img src="https://tinycomputers.io/images/z80-barden-book/cover-001.jpg" style="width: 400px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: right; margin: 25px;"&gt;&lt;/p&gt;
&lt;p&gt;In 1978, William Barden, Jr. set out to write a book with a threefold purpose: to acquaint the reader with the hardware of the Z80, to discuss its "almost overwhelming" software instruction set, and to describe the microcomputer systems being built around it. The result was &lt;em&gt;&lt;a href="https://baud.rs/5brWaW"&gt;The Z-80 Microcomputer Handbook&lt;/a&gt;&lt;/em&gt;, published by Howard W. Sams &amp;amp; Co., one of the most prolific technical publishers of the era. At just over 300 pages, Barden delivered on all three promises, producing a reference that served as both tutorial and encyclopedia for what was arguably the most important &lt;a href="https://baud.rs/DlwHJZ"&gt;microprocessor&lt;/a&gt; of the late 1970s.&lt;/p&gt;
&lt;p&gt;The copy I have is the eighth printing from 1985. Eight printings. The book was first published in 1978, and Howard W. Sams was still running the presses seven years later. I'll confess I hadn't fully appreciated that the Z80's popularity carried that kind of momentum into the mid-1980s, by which point Intel's 80286 had been on the market for three years and IBM's AT was already sitting on desks in corporate offices across America. Yet here was a book about an 8-bit processor from 1976, still selling briskly enough to justify another print run. That tells you something about the Z80's staying power, and something about the quality of Barden's handbook.&lt;/p&gt;
&lt;h3&gt;The Author and the Publisher&lt;/h3&gt;
&lt;p&gt;William Barden, Jr. was a prolific technical author who wrote extensively about microprocessors and microcomputers throughout the late 1970s and 1980s. His writing style sits in a comfortable middle ground between the dry precision of a Zilog datasheet and the conversational approachability of a hobbyist magazine column. He assumes the reader has some technical foundation but doesn't demand an electrical engineering degree. The prose is clear, methodical, and, when the subject matter allows, occasionally wry.&lt;/p&gt;
&lt;p&gt;Howard W. Sams &amp;amp; Co., a subsidiary of Macmillan, was headquartered in Indianapolis and had built a reputation as one of the go-to publishers for electronics and computing references. Their catalog included the famous &lt;em&gt;&lt;a href="https://baud.rs/samstechnical"&gt;Photofact&lt;/a&gt;&lt;/em&gt; service manuals and a long list of titles covering everything from transistor theory to amateur radio. A Sams book on your shelf carried a certain implicit endorsement: this was going to be technically sound, well-organized, and useful.&lt;/p&gt;
&lt;h3&gt;Three Sections, One Processor&lt;/h3&gt;
&lt;p&gt;Barden organized the book into three distinct sections, each approaching the Z80 from a different angle. Section I covers Z80 hardware: the architecture, interface signals and timing, addressing modes, instruction set, flags and arithmetic operations, interrupt sequences, and interfacing memory and I/O devices. Section II shifts to Z80 software, beginning with the assembly process itself and then working through the major instruction groups: data movement, arithmetic and logical operations, shifting and bit manipulation, list and table operations, subroutine calls, I/O and interrupt operations, and commonly used subroutines. Section III surveys five commercial microcomputer systems built around the Z80.&lt;/p&gt;
&lt;p&gt;This three-part structure gives the book a completeness that many competing references lacked. Readers who wanted to understand the Z80 at the silicon level could camp out in Section I. Programmers who needed to write assembly code had a thorough software reference in Section II. And anyone trying to decide which Z80 system to buy, or trying to understand what made these systems different from one another, could turn to Section III for a comparative tour.&lt;/p&gt;
&lt;h3&gt;The Hardware Foundation&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/z80-barden-book/architecture-016.jpg" style="width: 500px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: left; margin: 25px;"&gt;&lt;/p&gt;
&lt;p&gt;Section I opens with a concise but thorough treatment of the Z80's internal architecture. Barden walks the reader through the processor's register set: fourteen general-purpose 8-bit registers organized in two banks (A through L, and their primed counterparts A' through L'), plus the special-purpose registers: two index registers (IX and IY), the stack pointer, program counter, interrupt vector register, and memory refresh counter.&lt;/p&gt;
&lt;p&gt;The dual register bank architecture receives careful attention, and rightly so. The ability to swap between two complete register sets with a single EXX instruction was one of the Z80's most distinctive features. Barden explains not just the mechanics but the motivation: fast interrupt handling without the overhead of pushing and popping registers to the stack. For real-time applications (process control, data acquisition, communications) this was a significant advantage over the Intel 8080A, which required explicit save-and-restore sequences.&lt;/p&gt;
&lt;p&gt;The flag register documentation is similarly thorough. Each of the six testable flags (Sign, Zero, Half-carry, Parity/Overflow, Subtract, and Carry) gets individual treatment, with clear diagrams showing bit positions and the conditions under which each flag is set or cleared. Barden's flag register diagram on page 19 is the kind of figure you'd photocopy and tape to the wall above your workbench.&lt;/p&gt;
&lt;p&gt;Chapter 3 dives into the Z80's interface signals and timing with a level of detail that borders on the exhaustive. Every control signal is documented: MREQ, IORQ, RD, WR, RFSH, HALT, WAIT, INT, NMI, BUSRQ, BUSAK, and the rest. The timing diagrams for M1 cycles, memory read and write cycles, I/O cycles, interrupt acknowledge sequences, and bus request/acknowledge handshakes are presented with the precision needed for hardware designers wiring up actual systems. This is reference material, not light reading, but it's the kind of reference material you desperately need when your homebrew system isn't behaving and you're staring at an oscilloscope trace trying to figure out why.&lt;/p&gt;
&lt;p&gt;Chapter 4's treatment of addressing modes deserves special mention. The Z80 supported ten addressing modes: implied, immediate, extended immediate, register, register indirect, extended, modified page zero, relative, indexed, and bit addressing. Barden documents each with examples showing the instruction encoding at the bit level. The indexed addressing mode, using IX or IY plus a displacement byte, was a Z80 innovation that made structured data access far more practical than on the 8080A. Barden's diagrams showing multi-byte instruction formats, with op-codes, displacement values, and immediate data laid out byte by byte, are models of technical illustration.&lt;/p&gt;
&lt;p&gt;The instruction set itself, covered in Chapter 5, is presented in tabular form with every detail a programmer needs: mnemonic, symbolic operation, flag effects, op-code encoding in binary, byte count, machine cycle count, and T-state count. These tables span dozens of pages and represent the kind of painstaking documentation that made the book worth keeping within arm's reach during coding sessions. The eleven instruction groups (from 8-bit loads through block transfers, arithmetic operations, rotates and shifts, bit manipulation, jumps, calls, and I/O) are each given systematic treatment.&lt;/p&gt;
&lt;p&gt;Chapter 8 rounds out the hardware section with a practical discussion of interfacing memory and I/O devices to the Z80. The treatment of the Z80 PIO (Parallel Input/Output) chip is particularly detailed, covering all four operating modes with programming examples. Barden walks through the initialization sequences for each mode, the interrupt vector configuration, and the handshaking protocols, exactly the kind of information you'd struggle to extract from &lt;a href="https://baud.rs/MIPV1T"&gt;Zilog's own documentation&lt;/a&gt; without considerable effort.&lt;/p&gt;
&lt;h3&gt;The Software Perspective&lt;/h3&gt;
&lt;p&gt;Section II opens with what might be the most pedagogically effective chapter in the book: Chapter 9, the Z80 Assembler. Rather than jumping straight into assembly language syntax, Barden starts with raw machine language. He presents a trivial program (adding the numbers one through ten) first as a series of mnemonics, then as hand-assembled machine code with each op-code and operand byte spelled out in hexadecimal. He then shows the same program rewritten with a loop, and walks through the manual assembly process step by step: calculating instruction lengths, assigning memory addresses, resolving label references, filling in the binary encoding of each instruction.&lt;/p&gt;
&lt;p&gt;This is brilliant pedagogy. By forcing the reader through the pain of manual assembly (calculating that a JP NZ,LOOP instruction at address 0105H needs to encode the target address 0103H as bytes 03H and 01H in little-endian order) Barden ensures they understand exactly what an assembler does before they start using one. The transition from manual assembly to symbolic assembly language feels earned rather than arbitrary. When Barden introduces labels, pseudo-operations, expression evaluation, and the two-pass assembly process, the reader understands &lt;em&gt;why&lt;/em&gt; these features exist, not just how to use them.&lt;/p&gt;
&lt;p&gt;Chapters 10 through 15 systematically work through the instruction groups from a programmer's perspective. Each chapter takes a logical group (data movement, arithmetic and logic, shifting and bit manipulation, list and table operations, subroutines, I/O and CPU control) and provides detailed examples showing how the instructions are used in practice. The block transfer instructions (LDI, LDIR, LDD, LDDR) and block search instructions (CPI, CPIR) receive particularly good coverage, as these were among the Z80's most powerful features and had no equivalent in the 8080A instruction set.&lt;/p&gt;
&lt;p&gt;Chapter 16, covering commonly used subroutines, is where the book transitions from reference to practical cookbook. Barden provides complete, tested subroutine implementations for comparison, timing loops, multiply, divide, multiple-precision arithmetic, ASCII-to-binary conversion, base conversion, memory fill, string comparison, and table search. Each subroutine is documented with its entry conditions, exit conditions, and register usage, the kind of disciplined documentation that professional assembly programmers live by. The table search routine, using IX as a base pointer with entry size in DE, is a clean example of the Z80's indexed addressing mode earning its keep.&lt;/p&gt;
&lt;h3&gt;A Snapshot of the Ecosystem&lt;/h3&gt;
&lt;p&gt;Section III is where the book transforms from a processor reference into a historical document. In two chapters, Barden surveys five companies manufacturing Z80-based microcomputer systems: Zilog itself, Technical Design Labs (TDL), Cromemco, The Digital Group, and Radio Shack.&lt;/p&gt;
&lt;p&gt;The Zilog chapter covers the Z80 MCB (Microcomputer Board), a complete single-board computer measuring 7.7 by 7.75 inches. With 4K of dynamic RAM, up to 4K of EPROM or PROM, a PIO for parallel I/O, a USART for serial communication, and a CTC for timing, the MCB was a capable development platform. Barden documents the memory map, I/O port addressing, interrupt configuration, and the 1K monitor program with its eight commands for examining memory, setting breakpoints, and controlling program execution. The minimum MCB system (the board itself, a 5-volt power supply, and a Teletype ASR-33) was a complete development environment, albeit a spartan one.&lt;/p&gt;
&lt;p&gt;The Cromemco coverage reveals a more ambitious ecosystem. Their Z-1 and Z-2 systems were built around the &lt;a href="https://tinycomputers.io/posts/george-morrow-pioneer-of-personal-computing.html"&gt;S-100 bus&lt;/a&gt;, with the Z-2 offering a chassis with 21 card slots, a 30-amp power supply, and room for serious expansion. Cromemco's peripheral lineup included a TV DAZZLER for color graphics, a Digital Interface Board with analog-to-digital and digital-to-analog converters, and their BYTESAVER EPROM programmer board. Their CONTROL BASIC (a specialized BASIC interpreter designed for process control and automated testing) hints at the industrial applications that were already finding the Z80.&lt;/p&gt;
&lt;p&gt;The Digital Group section reveals a company taking a different approach: offering CPU boards for multiple processor families (Motorola 6800, MOS Technology 6502, Intel 8080A, and Z80) all interchangeable at the board level. Their Phi-Deck cassette storage system, with 800 bytes per second transfer rates and CRC error checking, was a notably sophisticated approach to the cassette storage problem.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/z80-barden-book/trs80-273.jpg" style="width: 500px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: left; margin: 25px;"&gt;&lt;/p&gt;
&lt;p&gt;And then there's Radio Shack. Barden's description of the TRS-80 stands out because of what it represented: a completely integrated, turnkey system that a consumer could purchase, take home, plug in, and immediately begin programming in BASIC. While the other systems in this chapter required varying degrees of assembly, configuration, and technical knowledge, the TRS-80 was designed for people who wanted to use a computer, not build one. With its 53-key keyboard, 12-inch monitor, cassette storage, and 4K of ROM containing a BASIC interpreter, it was a vision of the microcomputer's commercial future, even if its 64-character-by-16-line display and 4K of RAM seem quaint today.&lt;/p&gt;
&lt;p&gt;Reading these system descriptions in sequence, you can see the microcomputer market stratifying in real time. At one end, boards like Zilog's MCB served engineers and serious hobbyists who wanted maximum flexibility. In the middle, S-100 systems from Cromemco and others offered expandability with some degree of standardization. And at the consumer end, Radio Shack was proving that microcomputers could be mass-market products. All of them ran on the Z80.&lt;/p&gt;
&lt;h3&gt;The Intel Shadow&lt;/h3&gt;
&lt;p&gt;What makes the 1985 printing date so remarkable is the context in which someone would have been buying this book. By 1985, the microcomputer landscape had shifted dramatically from the world Barden documented in 1978.&lt;/p&gt;
&lt;p&gt;Intel had introduced the 8086 in 1978 (the same year this book was published) and its cost-reduced sibling, the 8088, in 1979. When IBM chose the 8088 for its Personal Computer in 1981, the x86 architecture gained a gravitational pull that would reshape the entire industry. The Intel 80286, launched in 1982, brought protected mode, a 16-megabyte address space, and hardware memory management. When IBM built the 80286 into the PC/AT in August 1984, it created what would become the standard business computer platform for years to come. The AT was fast, expandable, and, critically, backward compatible with the enormous library of software already written for the original PC.&lt;/p&gt;
&lt;p&gt;By 1985, the trajectory was clear. The x86 architecture was the future of personal computing. CP/M, which had been the dominant operating system for Z80 machines, was fading in the face of MS-DOS. The TRS-80 line was winding down. Cromemco had pivoted to 68000-based systems. The Z80's reign as the king of personal computing was effectively over.&lt;/p&gt;
&lt;p&gt;And yet the eighth printing rolled off the presses.&lt;/p&gt;
&lt;p&gt;The Z80 endured because personal computing was never the whole story. The processor had found its way into embedded systems, industrial controllers, point-of-sale terminals, scientific instruments, and countless other applications where its simplicity, low cost, and well-understood behavior were more valuable than raw performance. The Z80 didn't need a 16-megabyte address space to control a factory floor. It didn't need protected mode to run a cash register. It needed to be cheap, reliable, and thoroughly documented, and books like Barden's were part of that documentation ecosystem.&lt;/p&gt;
&lt;p&gt;There's also the educational angle. In 1985, the Z80 was still one of the best processors for &lt;em&gt;learning&lt;/em&gt; computer architecture. Its instruction set was complex enough to illustrate real-world design tradeoffs (accumulator-based operations, register pairs for 16-bit addressing, multiple addressing modes, condition flags) without being so complex as to overwhelm a student. Many universities and technical colleges were still teaching microprocessor courses using the Z80 well into the late 1980s. For those students and their instructors, Barden's handbook was still entirely relevant.&lt;/p&gt;
&lt;h3&gt;The Book as Reference&lt;/h3&gt;
&lt;p&gt;Evaluating &lt;em&gt;The Z-80 Microcomputer Handbook&lt;/em&gt; as a technical reference, it holds up remarkably well within its domain. The instruction set tables in Chapter 5 are comprehensive and clearly formatted, with every detail needed for hand-coding or verifying assembler output. The appendices (covering electrical specifications, an 8080-to-Z80 instruction cross-reference, a complete instruction summary, binary and hexadecimal tables, and ASCII codes) round out the reference material.&lt;/p&gt;
&lt;p&gt;The 8080/Z80 comparison in Appendix B is particularly useful for readers coming from the Intel side. Since the Z80 included the entire 8080A instruction set as a subset (using Zilog's own mnemonics rather than Intel's), this cross-reference served as a Rosetta Stone for programmers transitioning between the two architectures. Many Z80 systems needed to run software originally written for the 8080A, and understanding the mapping between Intel and Zilog mnemonics was a practical necessity.&lt;/p&gt;
&lt;p&gt;Where the book shows its age most clearly is in Section III. The specific microcomputer systems described (the Zilog MCB, the TDL ZPUTM and Xitan, the Cromemco Z-1 and Z-2, the Digital Group systems, and the TRS-80) are all long discontinued. But this is precisely what makes Section III valuable today: it's a primary source document of a hardware ecosystem that existed for a brief, vibrant moment and then vanished. You won't find this level of detail about the Digital Group's Phi-Deck cassette system or TDL's System Monitor Board in Wikipedia.&lt;/p&gt;
&lt;h3&gt;A Companion Piece&lt;/h3&gt;
&lt;p&gt;Readers of this site may notice a natural pairing with Steve Ciarcia's &lt;em&gt;&lt;a href="https://tinycomputers.io/posts/build-your-own-z80-computer-steve-ciarcia.html"&gt;Build Your Own Z80 Computer&lt;/a&gt;&lt;/em&gt; (&lt;a href="https://baud.rs/build-z80-ciarcia"&gt;Amazon&lt;/a&gt;), which we reviewed previously. Where Ciarcia's book is a construction manual (guiding the reader through building a complete Z80 system from power supply to CRT terminal) while Barden's handbook is a reference and survey. Ciarcia teaches you to &lt;em&gt;build&lt;/em&gt;; Barden teaches you to &lt;em&gt;understand&lt;/em&gt;. The two books complement each other almost perfectly, and it's easy to imagine a 1978-era hobbyist keeping both within reach: Barden's for looking up instruction encodings and timing specifications, Ciarcia's for wiring up the hardware to run them on.&lt;/p&gt;
&lt;p&gt;The difference in approach also reflects the different publishers. BYTE Books, which published Ciarcia, was rooted in the hobbyist magazine world and emphasized hands-on projects. Howard W. Sams had a longer tradition of comprehensive technical references. Each publisher played to its strengths.&lt;/p&gt;
&lt;h3&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;The Z-80 Microcomputer Handbook&lt;/em&gt; is not a book that will teach you to build a computer, nor is it one that will dazzle you with narrative flair. It is, instead, something arguably more valuable: a thorough, well-organized, clearly written reference to a processor and its ecosystem, produced at the moment when that ecosystem was at its peak. Barden's systematic approach (hardware first, then software, then systems) gives the reader a complete understanding of the Z80 world from silicon to finished product.&lt;/p&gt;
&lt;p&gt;That the book was still being printed in 1985, with the IBM AT already on the market and the 80386 just a year away, is a testament to both the Z80's remarkable longevity and the quality of Barden's work. Eight printings don't happen by accident. They happen because engineers, students, hobbyists, and embedded systems designers kept walking into bookstores and electronics shops and deciding that yes, they still needed this book.&lt;/p&gt;
&lt;p&gt;Nearly five decades after its original publication, &lt;em&gt;&lt;a href="https://baud.rs/5brWaW"&gt;The Z-80 Microcomputer Handbook&lt;/a&gt;&lt;/em&gt; remains a worthwhile read for anyone interested in the foundations of microcomputing. Paired with Rodnay Zaks' &lt;em&gt;&lt;a href="https://baud.rs/EsBekO"&gt;Programming the Z80&lt;/a&gt;&lt;/em&gt; for software depth and J.S. Walker's &lt;em&gt;&lt;a href="https://baud.rs/Ch4htI"&gt;Design a Z80 Computer&lt;/a&gt;&lt;/em&gt; for a modern practical build guide, it forms part of an essential Z80 library. The architecture it documents influenced a generation of processor designs. The assembly language techniques it teaches remain relevant for anyone working close to the metal. And the ecosystem it surveys (that brief, fertile period when a handful of small companies were inventing the personal computer industry in real time) deserves to be remembered in the detail that Barden provided.&lt;/p&gt;</description><category>assembly language</category><category>book review</category><category>microprocessors</category><category>retrocomputing</category><category>vintage computing</category><category>william barden</category><category>z80</category><category>zilog</category><guid>https://tinycomputers.io/posts/the-z80-microcomputer-handbook-william-barden.html</guid><pubDate>Thu, 05 Feb 2026 21:00:00 GMT</pubDate></item><item><title>How We Learned Hardware in 1983</title><link>https://tinycomputers.io/posts/how-we-learned-hardware-in-1983.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;p&gt;I recently spent an afternoon with a scanned PDF of &lt;em&gt;Z80 Applications&lt;/em&gt; by James W. Coffron, published by SYBEX in 1983. It's a 306-page guide to building microprocessor systems from discrete components. Reading it felt like archaeology, not because the technology is obsolete (the Z80 is still manufactured today), but because the &lt;em&gt;way&lt;/em&gt; it teaches is so foreign to how we learn hardware now.&lt;/p&gt;
&lt;p&gt;The book assumes you want to build a computer from chips. Not program one. Not configure one. &lt;em&gt;Build&lt;/em&gt; one, with a soldering iron, from a CPU, some RAM, some ROM, and a handful of 74LS-series logic gates. The gap between this and a modern "Getting Started with Arduino" tutorial is so vast it's almost difficult to articulate. But I'll try.&lt;/p&gt;
&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/how-we-learned-hardware-in-1983.mp3" type="audio/mpeg"&gt;
Your browser does not support the audio element.
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;21:44 · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;h3&gt;The Starting Point: What You're Expected to Know&lt;/h3&gt;
&lt;p&gt;Coffron's introduction sets the tone:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Have you ever imagined a new application for a microprocessor-controlled system, only to have the idea vanish under a torrent of technical details found in the data sheets? If so, you're not alone."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is a book for people who have already read the datasheets and gotten lost. The assumed baseline is that you understand what a microprocessor &lt;em&gt;is&lt;/em&gt;, what buses are, what memory addressing means. The book's job is to bridge the gap between "I understand the theory" and "I have a working system on my bench."&lt;/p&gt;
&lt;p&gt;Compare this to the Arduino "Blink" tutorial, which assumes you know how to plug in a USB cable. The entire hardware abstraction layer (the thing that makes &lt;code&gt;digitalWrite(13, HIGH)&lt;/code&gt; turn on an LED) is invisible. You don't need to know that pin 13 maps to PORTB bit 5 on the ATmega328P, or that setting that bit high puts 5V on the physical pin, or that there's a 220-ohm current-limiting resistor already on the board. You just call a function.&lt;/p&gt;
&lt;h3&gt;Chapter 1: Connecting a CPU to ROM&lt;/h3&gt;
&lt;p&gt;The first real chapter of Coffron's book is titled "Using the Z80 with ROM." Not &lt;em&gt;programming&lt;/em&gt; the Z80. Not &lt;em&gt;using&lt;/em&gt; an existing Z80 system. Literally: how do you wire a Z80 CPU chip to a 2716 EPROM so that the CPU can fetch instructions?&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/rom-schematic.png" alt="Complete schematic showing Z80 connected to 2716 EPROM with address decoding logic" style="max-width: 100%; margin: 20px 0;"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure 1.9 from the book: A complete schematic showing the connection between a 2716 EPROM and the Z80 microprocessor, including the logic used for enabling the memory device according to the memory map.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This single diagram requires understanding:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Address buses&lt;/strong&gt;: The Z80 has 16 address lines (A0-A15). The 2716 EPROM has 11 address inputs. How do you connect them? Which lines go where?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Data buses&lt;/strong&gt;: The Z80 has 8 data lines. The 2716 has 8 data outputs. These connect directly, but only when the chip is selected.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Chip select logic&lt;/strong&gt;: The 74LS138 is a 3-to-8 decoder. The book explains how to use the upper address lines (A11-A15) to generate chip select signals, so that different memory chips respond to different address ranges. This is &lt;em&gt;memory mapping&lt;/em&gt;, implemented in hardware.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Control signal generation&lt;/strong&gt;: The Z80 outputs MREQ (memory request) and RD (read). These must be combined with OR gates to generate MEMR, which enables the ROM's output buffers at the correct moment.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The timing diagram that accompanies this schematic shows exactly when each signal transitions:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/timing-diagram.png" alt="Timing diagram showing signal relationships for ROM read operation" style="max-width: 100%; margin: 20px 0;"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure 1.10: A timing diagram showing the important signal relationships. Address lines stabilize, MREQ goes low, RD goes low, data becomes valid, then everything releases.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This is page 12 of the book. We haven't written a single line of code yet. We're just trying to make the CPU able to read instructions from a ROM chip.&lt;/p&gt;
&lt;h3&gt;The Modern Equivalent: There Isn't One&lt;/h3&gt;
&lt;p&gt;On a Raspberry Pi, you don't connect the CPU to RAM. The RAM is soldered to the board. On an Arduino, the flash memory that holds your program is &lt;em&gt;inside the microcontroller chip&lt;/em&gt;. There's no schematic to draw because there's no external connection to make.&lt;/p&gt;
&lt;p&gt;When a modern tutorial says "upload your sketch," it's hiding:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The USB-to-serial converter that talks to the bootloader&lt;/li&gt;
&lt;li&gt;The bootloader that receives bytes and writes them to flash&lt;/li&gt;
&lt;li&gt;The flash memory controller that handles page erasure and programming&lt;/li&gt;
&lt;li&gt;The reset circuit that starts execution at the right address&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of this exists. It's just not something you're expected to understand, or even know about, to get started.&lt;/p&gt;
&lt;h3&gt;Dynamic RAM: Where It Gets Serious&lt;/h3&gt;
&lt;p&gt;Chapter 4 of Coffron's book covers dynamic RAM. This is where the real complexity lives.&lt;/p&gt;
&lt;p&gt;Static RAM is simple: you put an address on the bus, assert the chip select, and the data appears. Dynamic RAM is different. It stores bits in tiny capacitors that leak charge, so you have to refresh them periodically. It uses &lt;em&gt;multiplexed&lt;/em&gt; addressing to reduce pin count. You send the row address, strobe RAS (Row Address Strobe), then send the column address and strobe CAS (Column Address Strobe).&lt;/p&gt;
&lt;p&gt;The book dedicates an entire chapter to building a 16K x 8-bit dynamic RAM system using 4116 chips. This involves:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Address multiplexing hardware&lt;/strong&gt;: You need logic to switch between presenting the low address bits and the high address bits to the RAM chips at the right times.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;RAS/CAS timing generation&lt;/strong&gt;: The sequence of RAS and CAS signals must meet strict timing requirements. Too fast and the chips don't respond. Too slow and you waste cycles.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Refresh logic&lt;/strong&gt;: The Z80 has a built-in refresh counter (one of its innovations over the 8080), but you still need to understand when refresh cycles occur and how they interleave with normal memory access.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/ram-datasheet.png" alt="2114 Static RAM datasheet excerpt showing pinout and specifications" style="max-width: 100%; margin: 20px 0;"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure 2.6: A partial data sheet and block diagram for the 2114, a 1K x 4 static RAM. The book walks through every pin and its function.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Here's the key insight: in 1983, if you wanted more than a few kilobytes of RAM, you &lt;em&gt;had&lt;/em&gt; to use dynamic RAM. Static RAM was expensive. The 4116 gave you 16K bits for the same cost as 1K bits of static RAM. But you paid for it in complexity.&lt;/p&gt;
&lt;p&gt;Today? The Raspberry Pi 5 has 8 gigabytes of DDR4 RAM. The timing controller, refresh logic, and address multiplexing are all handled by a dedicated memory controller inside the Broadcom SoC. You don't configure it. You don't even see it. It just works.&lt;/p&gt;
&lt;h3&gt;Interrupts: Where Software Meets Hardware&lt;/h3&gt;
&lt;p&gt;Chapter 5 covers interrupts, and this is where Coffron's book becomes genuinely fascinating. The Z80 has three interrupt modes, and the book explains not just &lt;em&gt;what&lt;/em&gt; they do, but &lt;em&gt;how to wire the hardware&lt;/em&gt; to use them.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/interrupts-text.png" alt="Book page explaining interrupt handling and INT input" style="max-width: 100%; margin: 20px 0;"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The book explains the electrical characteristics of the INT input, how to mask interrupts in software, and the three interrupt modes.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Mode 2 interrupts are particularly elegant. When an interrupt occurs:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The interrupting device places a byte on the data bus&lt;/li&gt;
&lt;li&gt;The CPU reads this byte and combines it with the I register to form a 16-bit pointer&lt;/li&gt;
&lt;li&gt;This pointer addresses a table in memory containing the actual interrupt service routine address&lt;/li&gt;
&lt;li&gt;The CPU jumps to that address&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This means you can have up to 128 different interrupt vectors, each pointing to a different handler, with the interrupting &lt;em&gt;device&lt;/em&gt; choosing which one. The book shows how to set up the vector table in memory and how to wire multiple devices in a "daisy chain" priority scheme.&lt;/p&gt;
&lt;p&gt;In Arduino-land, you write &lt;code&gt;attachInterrupt(digitalPinToInterrupt(2), myFunction, RISING)&lt;/code&gt; and you're done. The hardware abstraction layer handles vector table management, priority, and dispatch. This is convenient. But you have no idea &lt;em&gt;why&lt;/em&gt; pin 2 can trigger an interrupt and pin 4 can't (on an Uno), or what happens inside the CPU when an interrupt fires.&lt;/p&gt;
&lt;h3&gt;The Peripheral Chips: A Complete Ecosystem&lt;/h3&gt;
&lt;p&gt;The Z80 wasn't just a CPU; it was the center of a chip family. Zilog made matching peripheral chips designed to work together:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Z80 PIO&lt;/strong&gt;: Parallel I/O controller with handshaking&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Z80 CTC&lt;/strong&gt;: Counter/Timer circuit for generating timing or counting events&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Z80 SIO&lt;/strong&gt;: Serial I/O for RS-232 communication&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Z80 DMA&lt;/strong&gt;: Direct Memory Access controller for fast block transfers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Coffron's book covers all of these, plus Intel's 8255 PIO and 8253 timer (which work with the Z80 despite being designed for the 8080).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/sio-init-code.png" alt="Z80 assembly code for initializing the SIO serial controller" style="max-width: 100%; margin: 20px 0;"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure 11.12: A Z80 program for initializing the Z80-SIO in an interrupt application. Every register must be programmed in the correct sequence.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Look at that initialization code. To set up a serial port, you have to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Write to the control port to select which internal register you want to access&lt;/li&gt;
&lt;li&gt;Write the actual configuration byte&lt;/li&gt;
&lt;li&gt;Repeat for each register (WR0 through WR7)&lt;/li&gt;
&lt;li&gt;Set up the interrupt vector&lt;/li&gt;
&lt;li&gt;Enable the receiver and transmitter separately&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The SIO has &lt;em&gt;eight&lt;/em&gt; write registers and &lt;em&gt;three&lt;/em&gt; read registers per channel, each with specific bit-level meanings. The book includes an appendix that's just register descriptions, twelve pages of bit definitions.&lt;/p&gt;
&lt;p&gt;Compare this to Arduino: &lt;code&gt;Serial.begin(9600)&lt;/code&gt;. One line. Done.&lt;/p&gt;
&lt;h3&gt;The Static Stimulus Tester: Debugging from First Principles&lt;/h3&gt;
&lt;p&gt;Chapter 12 is unlike anything you'll find in a modern tutorial. It describes building a "Static Stimulus Tester," a tool for debugging Z80 hardware by manually single-stepping the CPU.&lt;/p&gt;
&lt;p&gt;The idea is simple but radical: disconnect the crystal oscillator and replace it with a push-button. Each press advances the CPU by one clock cycle. Add LED displays for the address bus, data bus, and control signals. Now you can literally watch the CPU execute, one clock tick at a time.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/sio-code.png" alt="Assembly code example showing register operations" style="max-width: 100%; margin: 20px 0;"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The book includes assembly language examples throughout, always tied to specific hardware configurations.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This is how engineers debugged hardware in 1983. No logic analyzer (those cost more than cars). No JTAG. No printf debugging. You built a tool that let you see electricity moving through your circuit, and you traced the problem by hand.&lt;/p&gt;
&lt;p&gt;Modern development boards have LEDs. They blink to show activity. But they're diagnostic indicators, not debugging tools. If your Arduino doesn't work, you use the serial monitor to print messages. If that doesn't work, you post on Stack Overflow. The idea of building a hardware tool to watch your CPU execute instruction-by-instruction is genuinely alien to modern practice.&lt;/p&gt;
&lt;h3&gt;The Economics of Learning&lt;/h3&gt;
&lt;p&gt;It's worth considering what it actually cost to learn hardware in 1983.&lt;/p&gt;
&lt;p&gt;The book itself was probably \$15-20. But to actually &lt;em&gt;use&lt;/em&gt; it, you needed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;Z80 CPU&lt;/strong&gt;: about \$10-15 in 1983 dollars&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2716 EPROMs&lt;/strong&gt;: \$5-10 each, and you'd need several&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2114 static RAM chips&lt;/strong&gt;: \$3-5 each, eight for a basic system&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;74LS-series logic chips&lt;/strong&gt;: \$0.50-2 each, dozens needed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sockets, capacitors, resistors, wire&lt;/strong&gt;: another \$20-30&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A prototype board or wirewrap supplies&lt;/strong&gt;: \$10-50&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An EPROM programmer&lt;/strong&gt;: \$100-300 for a basic unit&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An oscilloscope&lt;/strong&gt;: \$300-1000 for something usable&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A power supply&lt;/strong&gt;: \$30-50&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A minimal setup to follow along with Coffron's book would run \$200-400 in 1983 dollars, roughly \$600-1200 in 2026 money. A serious hobbyist setup with a decent scope and programmer could easily hit \$1000-2000 (1983), or \$3000-6000 adjusted.&lt;/p&gt;
&lt;p&gt;Compare that to an Arduino Uno: \$25. A Raspberry Pi: \$35-75. A USB cable you probably already own. Free software. Free tutorials. Free community support on forums and Discord.&lt;/p&gt;
&lt;p&gt;The democratization isn't just about abstraction; it's about cost. In 1983, learning hardware was expensive enough that it filtered for a certain kind of person: someone with disposable income, access to electronics suppliers, and enough space for a workbench. Today, a teenager with a part-time job can afford to experiment.&lt;/p&gt;
&lt;p&gt;This matters. The people who learned hardware in 1983 were a self-selected group. The people who learn today are everyone.&lt;/p&gt;
&lt;h3&gt;What We Gained and What We Lost&lt;/h3&gt;
&lt;p&gt;I want to be clear: I'm not arguing that the 1983 approach is &lt;em&gt;better&lt;/em&gt;. The barriers to entry were enormous. Building a working Z80 system from Coffron's book required:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Understanding digital logic (AND, OR, NAND gates)&lt;/li&gt;
&lt;li&gt;Reading timing diagrams&lt;/li&gt;
&lt;li&gt;Calculating address decoding logic&lt;/li&gt;
&lt;li&gt;Soldering dozens of chips onto a board&lt;/li&gt;
&lt;li&gt;Having access to an EPROM programmer&lt;/li&gt;
&lt;li&gt;Writing assembly language&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This took months of study and significant equipment investment. The number of people who could do it was small.&lt;/p&gt;
&lt;p&gt;Today, a complete beginner can have an LED blinking on an Arduino within an hour of opening the box. They can connect sensors, motors, and displays without understanding the underlying protocols. They can build genuinely useful things (home automation, art installations, scientific instruments) without ever reading a timing diagram.&lt;/p&gt;
&lt;p&gt;This democratization is valuable. More people building more things is good.&lt;/p&gt;
&lt;p&gt;But something &lt;em&gt;is&lt;/em&gt; lost.&lt;/p&gt;
&lt;p&gt;The modern approach creates a gap between "user" and "understander." You can use an Arduino without understanding it. You can use a Raspberry Pi as a Linux computer without knowing it has GPIO pins. You can run machine learning models without understanding matrix multiplication.&lt;/p&gt;
&lt;p&gt;In 1983, there was no such gap. If you were using a microprocessor, you understood microprocessors. You had to. The abstraction layers didn't exist yet.&lt;/p&gt;
&lt;h3&gt;The Pedagogical Difference&lt;/h3&gt;
&lt;p&gt;Coffron's book teaches hardware by building it. Each chapter adds another subsystem: ROM, then RAM, then I/O ports, then interrupts, then timers, then serial communication. By the end, you understand how all the pieces connect because you &lt;em&gt;connected them yourself&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Modern tutorials teach by doing. "Make this LED blink. Now make it blink faster. Now add a button. Now read a temperature sensor." The complexity is hidden, revealed only when you hit the limits of the abstraction.&lt;/p&gt;
&lt;p&gt;Both approaches work. But they produce different kinds of understanding.&lt;/p&gt;
&lt;p&gt;Someone who learned from Coffron's book can troubleshoot hardware. They can look at an oscilloscope trace and identify a timing problem. They can read a datasheet for a new chip and understand how to integrate it. They have a mental model of what the electricity is doing.&lt;/p&gt;
&lt;p&gt;Someone who learned from Arduino tutorials can build things. They can iterate quickly, trying different sensors and actuators until something works. They can find libraries that solve their problems and examples they can modify. They have a mental model of what the &lt;em&gt;software&lt;/em&gt; is doing.&lt;/p&gt;
&lt;p&gt;Neither is wrong. But they're not the same thing.&lt;/p&gt;
&lt;h3&gt;What Would 1983 Make of 2026?&lt;/h3&gt;
&lt;p&gt;If you dropped a Raspberry Pi 5 on James Coffron's desk in 1983, what would he see?&lt;/p&gt;
&lt;p&gt;A computer with four 64-bit CPU cores running at 2.4 GHz. Eight gigabytes of RAM. Gigabit Ethernet. Bluetooth. WiFi. USB. HDMI. A GPU capable of 4K video. Storage measured in terabytes.&lt;/p&gt;
&lt;p&gt;And no way to understand any of it from first principles.&lt;/p&gt;
&lt;p&gt;The SoC at the heart of a Raspberry Pi contains billions of transistors. The memory controller alone is more complex than every computer that existed in 1983 combined. The protocols (USB 3.0, HDMI 2.0, PCIe) are specified in documents thousands of pages long.&lt;/p&gt;
&lt;p&gt;You can't build a Raspberry Pi from discrete components. You can't even build the chips that go into it without a billion-dollar fab. The abstraction isn't just convenient; it's &lt;em&gt;necessary&lt;/em&gt;. The complexity has exceeded human ability to hold it in one head.&lt;/p&gt;
&lt;p&gt;This is progress. But it's also a kind of loss.&lt;/p&gt;
&lt;h3&gt;Closing Thoughts&lt;/h3&gt;
&lt;p&gt;I keep Coffron's book on my shelf not because I'm going to build a Z80 system (though I might, someday), but because it represents a way of thinking about computers that's worth preserving.&lt;/p&gt;
&lt;p&gt;The Z80 is comprehensible. One person can understand all of it: every instruction, every pin, every timing requirement. The peripheral chips are comprehensible too. A moderately determined person can read the datasheets and &lt;em&gt;know&lt;/em&gt; exactly what the hardware is doing.&lt;/p&gt;
&lt;p&gt;Modern computers are not comprehensible in this way. They're usable, powerful, and democratized. But they're also opaque, their complexity hidden behind layers of abstraction that most users will never penetrate.&lt;/p&gt;
&lt;p&gt;Both things can be true. We can celebrate Arduino for making electronics accessible while acknowledging that something is different (not worse, but different) about a world where understanding and using have become separate activities.&lt;/p&gt;
&lt;p&gt;If you want to experience what hardware education was like in 1983, find a copy of Coffron's book. It's out of print, but scanned PDFs exist. Read chapter 1. Draw the schematic on paper. Trace the signals with your finger. Try to understand not just &lt;em&gt;what&lt;/em&gt; the circuit does, but &lt;em&gt;why&lt;/em&gt; it works.&lt;/p&gt;
&lt;p&gt;Then open the Arduino IDE and type &lt;code&gt;digitalWrite(13, HIGH)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Both are valid. But they're not the same thing.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Resources&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;The Books&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://baud.rs/z80-applications"&gt;&lt;em&gt;Z80 Applications&lt;/em&gt; by James W. Coffron&lt;/a&gt;: The 1983 SYBEX book discussed in this article. Used copies occasionally appear.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/build-z80-ciarcia"&gt;&lt;em&gt;Build Your Own Z80 Computer&lt;/em&gt; by Steve Ciarcia&lt;/a&gt;: Another classic from 1981, showing how to construct a complete Z80 system from scratch.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/circuit-cellar"&gt;&lt;em&gt;The Best of Ciarcia's Circuit Cellar&lt;/em&gt;&lt;/a&gt;: A collection of Steve Ciarcia's legendary BYTE magazine projects.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The Modern Approach&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://baud.rs/arduino-uno"&gt;Arduino Uno R3&lt;/a&gt;: The official board. Plug it in, upload a sketch, blink an LED.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/elegoo-kit"&gt;ELEGOO UNO R3 Most Complete Starter Kit&lt;/a&gt;: 200+ components with tutorials. Everything you need to get started.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/raspberry-pi-5"&gt;Raspberry Pi 5 (8GB)&lt;/a&gt;: The computer that would have seemed like science fiction in 1983.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The 1983 Toolkit&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://baud.rs/hakko-soldering"&gt;Hakko FX888D Soldering Station&lt;/a&gt;: A quality soldering station for building hardware the old-fashioned way.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/rigol-scope"&gt;Rigol DS1054Z Oscilloscope&lt;/a&gt;: A 4-channel 50MHz scope. In 1983, this capability would have cost thousands.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/t48-programmer"&gt;T48 Universal Programmer&lt;/a&gt;: Programs EPROMs, EEPROMs, and microcontrollers. Supports the vintage 27Cxxx series with high-voltage programming.&lt;/li&gt;
&lt;/ul&gt;</description><category>1983</category><category>arduino</category><category>electronics</category><category>embedded systems</category><category>hardware</category><category>history</category><category>microprocessors</category><category>raspberry pi</category><category>retrocomputing</category><category>z80</category><guid>https://tinycomputers.io/posts/how-we-learned-hardware-in-1983.html</guid><pubDate>Sat, 31 Jan 2026 16:00:00 GMT</pubDate></item><item><title>Build Your Own Z80 Computer: A Classic Guide to DIY Microcomputing</title><link>https://tinycomputers.io/posts/build-your-own-z80-computer-steve-ciarcia.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;p&gt;In 1981, Steve Ciarcia published what would become one of the most influential books in the homebrew computing movement: &lt;em&gt;&lt;a href="https://baud.rs/2sSEi4"&gt;Build Your Own Z80 Computer: Design Guidelines and Application Notes&lt;/a&gt;&lt;/em&gt;. Published by BYTE Books, this comprehensive guide took readers on a journey from understanding basic power supply design all the way through constructing a fully functional microcomputer called ZAP (Z80 Applications Processor). More than four decades later, the book remains a fascinating window into an era when building your own computer wasn't just possible, it was expected.&lt;/p&gt;
&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/z80_ciarcia_tts.mp3" type="audio/mpeg"&gt;
Your browser does not support the audio element.
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;27 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;&lt;img src="https://tinycomputers.io/images/z80-book/cover-001.jpg" style="width: 400px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: right; margin: 25px;"&gt;&lt;/p&gt;
&lt;h3&gt;The Author and His Mission&lt;/h3&gt;
&lt;p&gt;Steve Ciarcia was already well-known to readers of BYTE magazine through his popular "Ciarcia's Circuit Cellar" column, where he regularly presented do-it-yourself hardware projects. His columns had generated substantial reader interest, revealing that despite the growing availability of assembled personal computers, there remained a dedicated community of technically-minded individuals who wanted to understand what was inside their machines, and build them themselves.&lt;/p&gt;
&lt;p&gt;Ciarcia wrote in the introduction: "Build Your Own Z80 Computer is a book written for technically minded individuals who are interested in knowing what is inside a microcomputer. It is for persons who, already possessing a basic understanding of electronics, want to build rather than purchase a computer."&lt;/p&gt;
&lt;p&gt;This wasn't a book for beginners who had never held a soldering iron, nor was it an engineering textbook filled with impenetrable mathematics. Ciarcia positioned it squarely in the middle: practical enough for the determined hobbyist, technical enough to provide genuine understanding.&lt;/p&gt;
&lt;p&gt;The book also reflected Ciarcia's pragmatic philosophy about engineering design. He shared an anecdote about having lunch with the designer of a major personal computer system, expecting to hear about months of careful optimization and instruction set analysis. Instead, he learned the designer had simply been given two months to produce something manufacturable, and had built the system around the microprocessor he happened to already own. "So much for textbook engineering design," Ciarcia noted wryly, while acknowledging that hobbyists doing hand-wiring needed to be more deliberate in their choices.&lt;/p&gt;
&lt;h3&gt;Why the Z80?&lt;/h3&gt;
&lt;p&gt;The choice of the Z80 microprocessor as the heart of the ZAP computer wasn't arbitrary. Ciarcia explained that when building a microcomputer from scratch, several criteria must be carefully considered: circuit complexity, cost, and software compatibility.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://baud.rs/lMHaCa"&gt;Z80&lt;/a&gt; (also available in &lt;a href="https://baud.rs/pKTtKO"&gt;5-packs&lt;/a&gt;), introduced by Zilog in 1976, offered a compelling combination of features. It executed the complete instruction set of the Intel 8080A while adding significant enhancements of its own. Where the 8080A required multiple support chips (the 8224 clock driver and 8228 bus controller) to function, the Z80 integrated much of this functionality on-chip. This meant fewer components, simpler wiring, and reduced opportunities for errors, all critical considerations for hand-wired projects.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/z80-book/z80-pinout-diagram-037.jpg" style="width: 500px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: left; margin: 25px;"&gt;&lt;/p&gt;
&lt;p&gt;The Z80's architecture included 158 instructions (compared to the 8080A's 78), dual register sets for rapid context switching, index registers for sophisticated addressing modes, and a built-in memory refresh counter for dynamic RAM. The processor came in a standard 40-pin dual inline package, with clearly defined buses: a 16-bit address bus capable of addressing 64K bytes of memory, an 8-bit bidirectional data bus, and a comprehensive control bus.&lt;/p&gt;
&lt;p&gt;Ciarcia also noted the practical reality of the personal computer market: "The fact that so many personal computers are in use has established de facto standardization of central processor choice." The Z80 was already powering popular systems like the TRS-80, Sinclair ZX80, and numerous CP/M machines, which meant abundant software and documentation were available.&lt;/p&gt;
&lt;p&gt;The Z80 compared favorably to its contemporaries. While the Motorola 6800 and MOS Technology 6502 were capable processors, Ciarcia's analysis considered three key factors: circuit complexity (keeping components to a minimum to reduce wiring errors), cost (finding the sweet spot between cheap discrete logic and expensive LSI), and software compatibility (ensuring the user could benefit from existing programs and development tools). The Z80 struck an optimal balance across all three dimensions for the hand-wired builder.&lt;/p&gt;
&lt;h3&gt;Starting With the Power Supply&lt;/h3&gt;
&lt;p&gt;In a decision that reveals much about Ciarcia's pedagogical approach, the book begins not with the glamorous central processor but with the humble power supply. His reasoning was both practical and psychological: "This is a good way to test ability and provide immediate positive reinforcement from successful construction."&lt;/p&gt;
&lt;p&gt;The ZAP computer required three DC voltage rails: +5V at 5 amps for the digital logic, and ±12V at 1 amp each for peripheral interfaces and analog circuits. Ciarcia devoted an entire chapter to power supply design, explaining transformer selection, full-wave bridge rectification, filter capacitor sizing, and three-terminal voltage regulators.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/z80-book/power-supply-schematic-021.jpg" style="width: 600px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: right; margin: 25px;"&gt;&lt;/p&gt;
&lt;p&gt;The technical depth here is remarkable. Readers learned about ripple voltage calculations, the relationship between peak voltage and steady-state voltage, and why a 25% ripple factor was recommended for the filter capacitor. The explanation of three-terminal regulators like the μA78H05 covered not just their use but their internal operation: bandgap references, error amplifiers, and thermal protection circuits.&lt;/p&gt;
&lt;p&gt;Perhaps most valuably, Ciarcia addressed overvoltage protection. Recognizing that a regulator failure could send dangerous voltage spikes to sensitive logic circuits, he included detailed designs for SCR-based "crowbar" circuits that would short the output to ground and blow a fuse before damage could occur. This wasn't theoretical paranoia; it was the kind of practical wisdom that came from experience with real-world failures.&lt;/p&gt;
&lt;p&gt;The chapter also tackled thermal management, a subject often glossed over in hobbyist literature. Ciarcia explained heat sink calculations, thermal resistance from junction to ambient, and the importance of forced air cooling for high-current regulators. His advice was characteristically practical: "I hate red-hot power supplies." Rather than agonize over precise thermal calculations, he recommended adequate heat sinking combined with a fan. The additional cost of robust cooling was, he noted, far less than the cost of replacing fried computer components.&lt;/p&gt;
&lt;p&gt;Layout considerations received equal attention. Ciarcia explained how improper placement of filter capacitors could induce ripple on the output voltage, and how resistance in power distribution wiring could create different voltage levels at different points in the circuit. His solution (heavy gauge wire, single-point grounding, and dedicated bus strips) reflected the kind of hard-won practical knowledge that separates reliable designs from troublesome ones.&lt;/p&gt;
&lt;h3&gt;Understanding the Z80's Internal Architecture&lt;/h3&gt;
&lt;p&gt;Chapter 3 provides what amounts to a complete Z80 programmer's reference. Ciarcia walked through every aspect of the processor's architecture: the accumulator and flag registers, general-purpose register pairs (BC, DE, HL), special-purpose registers (program counter, stack pointer, index registers IX and IY), and the interrupt and refresh registers.&lt;/p&gt;
&lt;p&gt;The dual register set architecture received particular attention. The Z80 contained two complete sets of general-purpose registers (main and alternate) that could be swapped with single exchange instructions. This feature enabled rapid interrupt handling and context switching, capabilities that made the Z80 attractive for real-time applications.&lt;/p&gt;
&lt;p&gt;The instruction set documentation is exhaustive, rivaling dedicated references like Rodnay Zaks' &lt;em&gt;&lt;a href="https://baud.rs/EsBekO"&gt;Programming the Z80&lt;/a&gt;&lt;/em&gt; (&lt;a href="https://baud.rs/kua7Ko"&gt;PDF&lt;/a&gt;) and Ramesh Gaonkar's &lt;em&gt;&lt;a href="https://baud.rs/EJ28pl"&gt;The Z80 Microprocessor: Architecture, Interfacing, Programming and Design&lt;/a&gt;&lt;/em&gt;. Every instruction is described with its mnemonic, operation, affected flags, byte encoding, cycle count, and timing states. Load and exchange instructions, arithmetic and logical operations, block transfer and search commands, rotate and shift operations, bit manipulation, jumps, calls, returns, and I/O instructions. All are covered with the detail needed for hand assembly of machine code.&lt;/p&gt;
&lt;p&gt;Ciarcia also explained the hardware interface: the meaning of every pin on the Z80's 40-pin package. Control signals like MREQ (memory request), IORQ (I/O request), RD (read), WR (write), and RFSH (refresh) are described in terms of their logical function and timing relationships. The interrupt system, with its three modes and maskable/non-maskable hierarchy, receives thorough treatment.&lt;/p&gt;
&lt;h3&gt;Building the Basic Computer&lt;/h3&gt;
&lt;p&gt;Chapter 4, "Build Your Own Computer: Start With the Basics," transitions from theory to practice. The approach is methodical: construct the minimum viable system first, verify its operation, then expand.&lt;/p&gt;
&lt;p&gt;The basic ZAP configuration included the Z80 processor, 1K of EPROM containing the monitor program, 2K of static RAM for user programs and stack, address decoding logic, and a hexadecimal display with keyboard for interaction. Ciarcia deliberately avoided elaborate peripherals in the initial build, focusing instead on achieving a working foundation.&lt;/p&gt;
&lt;p&gt;Memory architecture was a central concern. Ciarcia explained memory mapping, the allocation of the Z80's 64K address space among ROM, RAM, and I/O devices. For ZAP, the memory map placed the 1K monitor EPROM at addresses 0000-03FF (where the Z80 begins execution after reset), with RAM starting at address 2000 hex.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/z80-book/basic-zap-120.jpg" style="width: 500px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: left; margin: 25px;"&gt;&lt;/p&gt;
&lt;p&gt;The discussion of memory devices compared available options. The 2716 EPROM (2K × 8) and 2114 static RAM (1K × 4) were recommended for their balance of capacity, availability, and ease of use. Ciarcia explained why static RAM was preferred over dynamic RAM for this application: dynamic RAM required refresh logic that, while supported by the Z80, added complexity inappropriate for a learning project.&lt;/p&gt;
&lt;p&gt;Address decoding used simple 74LS138 three-to-eight decoders, with higher address lines selecting among memory banks and lower lines addressing within each device. This straightforward approach traded some address space efficiency for wiring simplicity.&lt;/p&gt;
&lt;h3&gt;The Peripheral Ecosystem&lt;/h3&gt;
&lt;p&gt;With the basic computer operational, Chapter 5 expanded the system with practical peripherals. The hexadecimal display and keyboard provided the most basic human interface, allowing users to examine and modify memory contents, run programs, and debug code.&lt;/p&gt;
&lt;p&gt;The RS-232C serial interface opened connections to terminals and other equipment. Ciarcia used the COM2017 UART (Universal Asynchronous Receiver/Transmitter) to handle serial data formatting, baud rate generation, and handshaking. The interface supported standard rates from 110 to 9600 baud, making ZAP compatible with widely available terminals.&lt;/p&gt;
&lt;p&gt;For mass storage, the book included a cassette interface design. In 1981, audio cassettes were the affordable mass storage medium for personal computers. Ciarcia's design used the Kansas City Standard (300 baud), encoding data as audio tones that could be recorded on any audio cassette recorder. While primitive by modern standards, this approach made program storage accessible without expensive disk drives.&lt;/p&gt;
&lt;h3&gt;The ZAP Monitor Software&lt;/h3&gt;
&lt;p&gt;Chapter 6 revealed the software that breathed life into the hardware. The ZAP monitor was a 1K program stored in EPROM that coordinated system operations and provided a command interface for the user.&lt;/p&gt;
&lt;p&gt;The monitor's capabilities included:
- Memory examination and modification
- Program execution from specified addresses
- Register display and modification
- Block memory operations (move, fill, compare)
- Cassette load and save operations
- Breakpoint debugging support&lt;/p&gt;
&lt;p&gt;Ciarcia presented the complete source code with detailed annotations. The listings served dual purposes: they documented the specific monitor implementation, and they provided extensive examples of Z80 assembly language programming. Readers learned about interrupt handling, I/O programming, data structure manipulation, and subroutine organization, not from abstract examples but from working, useful code.&lt;/p&gt;
&lt;p&gt;The flow diagrams accompanying the source code illustrated program logic visually, a documentation technique that helped readers understand the relationships between code sections before diving into instruction-level details.&lt;/p&gt;
&lt;h3&gt;Programming an EPROM&lt;/h3&gt;
&lt;p&gt;Chapter 7 addressed a critical practical concern: how to get the monitor program into the EPROM in the first place. This chicken-and-egg problem (needing a programmed EPROM to have a working computer, but needing a working computer to program an EPROM) was solved through a dedicated EPROM programmer built as part of the ZAP system.&lt;/p&gt;
&lt;p&gt;Ciarcia's EPROM programmer design could program 2708, 2716, and similar devices. The programming process required specific voltage sequences and timing, all explained in sufficient detail for readers to understand the underlying physics of EPROM operation. For those who preferred to avoid this bootstrap problem, Ciarcia noted that programmed EPROMs were available by mail order.&lt;/p&gt;
&lt;h3&gt;Connecting ZAP to the Real World&lt;/h3&gt;
&lt;p&gt;Chapter 8, "Connecting ZAP to the Real World," transformed the computer from an abstract digital machine into a practical tool for measurement and control. This chapter covered analog-to-digital and digital-to-analog conversion, enabling ZAP to interact with continuous physical quantities.&lt;/p&gt;
&lt;p&gt;The A/D converter design used the ADC0804, an 8-bit successive approximation converter that could digitize analog signals for processing. Applications ranged from simple voltage measurement to data logging systems that could record environmental parameters over time.&lt;/p&gt;
&lt;p&gt;On the output side, D/A conversion allowed ZAP to generate analog signals for control applications. Ciarcia included a particularly interesting application: digital speech synthesis using the SC-01 speech synthesizer chip. By sending phoneme codes from the Z80 to the speech chip, ZAP could produce intelligible spoken output, a capability that seemed almost magical in 1981.&lt;/p&gt;
&lt;h3&gt;Building a CRT Terminal&lt;/h3&gt;
&lt;p&gt;The final major project, Chapter 9, was arguably the most ambitious: a complete CRT terminal. Rather than connecting ZAP to an expensive commercial terminal, readers could build their own using the CRT5027 video timer/controller and CRT8002 video attributes controller.&lt;/p&gt;
&lt;p&gt;The terminal design supported an 80-column by 24-row display, the standard format that would dominate computing for decades. Character generation used a ROM-based approach, with dot-matrix patterns for the full ASCII character set. The design included cursor control, scrolling, and basic display attributes.&lt;/p&gt;
&lt;p&gt;This project integrated many skills developed earlier in the book: power supply design, digital logic, memory interfacing, and RS-232 communication. The completed terminal could serve not only ZAP but any computer with a serial port, making it a genuinely useful piece of equipment.&lt;/p&gt;
&lt;p&gt;The terminal design included detailed worksheets for calculating timing parameters: horizontal and vertical sync widths, blanking intervals, and character clock rates. These calculations showed readers how video timing worked at a fundamental level, demystifying what seemed like magic to many hobbyists. The result was not just a working terminal but genuine understanding of raster-scan display technology.&lt;/p&gt;
&lt;h3&gt;The Appendices: Reference Material&lt;/h3&gt;
&lt;p&gt;The appendices transformed the book from a tutorial into a reference. Appendix A covered construction techniques: wire-wrapping, soldering, and printed circuit board fabrication. Appendix B provided ASCII code tables. Appendix C collected manufacturers' specification sheets for key components, eliminating the need to track down separate datasheets.&lt;/p&gt;
&lt;p&gt;Appendix D contained the complete ZAP operating system listing, while Appendix E provided Z80 CPU technical specifications including electrical characteristics, timing diagrams, and the complete instruction set summary. The glossary and index made the book's wealth of information accessible for quick reference.&lt;/p&gt;
&lt;h3&gt;The Historical Context&lt;/h3&gt;
&lt;p&gt;To fully appreciate &lt;em&gt;Build Your Own Z80 Computer&lt;/em&gt;, one must understand the computing landscape of 1981. The MITS Altair 8800 had kicked off the personal computer revolution just six years earlier. The Apple II was four years old. The Commodore PET and TRS-80 were both recent arrivals. Computing was still new enough that the question "Should I build or buy?" was legitimate.&lt;/p&gt;
&lt;p&gt;Commercial computers were expensive (often thousands of dollars) while components were relatively cheap. A skilled hobbyist with time could build substantial systems for a fraction of the commercial cost. More importantly, building your own computer meant you understood it completely. When something broke, you could fix it. When you wanted to expand it, you knew how. This self-reliance was both necessity and philosophy in the early hobbyist community.&lt;/p&gt;
&lt;p&gt;The book also appeared during the golden age of electronics magazines. BYTE, Radio Electronics, Popular Electronics, and others featured monthly construction articles. Readers expected detailed schematics, parts lists, and construction notes. Ciarcia's book was, in many ways, an extended version of the magazine articles that had made him famous, but comprehensive enough to guide a reader through an entire computer system rather than a single peripheral.&lt;/p&gt;
&lt;h3&gt;The Book's Lasting Significance&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;Build Your Own Z80 Computer&lt;/em&gt; appeared at a transitional moment in computing history. The homebrew era of the mid-1970s, when building your own computer was often the only affordable option, was giving way to an era of mass-produced personal computers. The IBM PC would debut later in 1981, accelerating the shift toward standardized, commercially manufactured systems.&lt;/p&gt;
&lt;p&gt;Yet Ciarcia's book served a purpose beyond mere necessity. It educated a generation of engineers and enthusiasts in the fundamentals of digital design. Readers who worked through ZAP gained deep understanding of computer architecture, not just how to use a computer, but how computers actually work at the hardware level. Many professional engineers who came of age in the 1980s cite this book (or Ciarcia's magazine columns) as formative influences on their careers.&lt;/p&gt;
&lt;p&gt;The book also demonstrated that complex systems could be understood through systematic decomposition. By building each subsystem separately, testing it in isolation, and then integrating the pieces, even ambitious projects became manageable. This approach to complexity management remains relevant today, whether one is building embedded systems, designing software architectures, or troubleshooting network infrastructure.&lt;/p&gt;
&lt;p&gt;The pedagogical approach Ciarcia employed (theory interspersed with practice, abstract concepts grounded in concrete examples) anticipated modern project-based learning. Readers weren't just told how a regulator worked; they built one and measured its performance. They weren't just taught about memory addressing; they wired decode logic and watched the computer access specific memory locations. This hands-on approach created understanding that mere reading could never provide.&lt;/p&gt;
&lt;p&gt;For the retrocomputing enthusiast, &lt;em&gt;Build Your Own Z80 Computer&lt;/em&gt; offers both historical insight and practical guidance. The Z80 remains available, its instruction set unchanged from 1976. Modern implementations on FPGAs and emulators keep the architecture accessible. Projects like the &lt;a href="https://baud.rs/DrkzSa"&gt;RC2014&lt;/a&gt; and other Z80-based single-board computers carry forward the tradition of building your own system. For those inspired to design their own Z80 system from scratch, J.S. Walker's &lt;em&gt;&lt;a href="https://baud.rs/Ch4htI"&gt;Design a Z80 Computer&lt;/a&gt;&lt;/em&gt; provides a modern practical guide to discrete-logic computer design. And Ciarcia's thorough documentation makes it possible to understand, and even construct, systems that defined the early personal computer era.&lt;/p&gt;
&lt;p&gt;Steve Ciarcia went on to found Circuit Cellar magazine and continued contributing to the hobbyist electronics community for decades. But &lt;em&gt;Build Your Own Z80 Computer&lt;/em&gt; remains perhaps his most complete and influential work, a comprehensive guide that took readers from bare components to working computer, explaining every step along the way.&lt;/p&gt;
&lt;p&gt;The book stands as a testament to an era when understanding technology meant building it yourself, one wire at a time. In an age of sealed devices and cloud computing, there's something deeply satisfying about returning to first principles, about understanding that a computer is ultimately just silicon responding to voltages, executing instructions one cycle at a time, exactly as Steve Ciarcia explained over four decades ago.&lt;/p&gt;</description><category>diy electronics</category><category>hardware design</category><category>microprocessors</category><category>retrocomputing</category><category>steve ciarcia</category><category>vintage computing</category><category>z80</category><guid>https://tinycomputers.io/posts/build-your-own-z80-computer-steve-ciarcia.html</guid><pubDate>Mon, 19 Jan 2026 14:00:00 GMT</pubDate></item><item><title>CP/M 2.2 on Physical Hardware: RetroShield Z80 with 1MB DRAM and SD Card Storage</title><link>https://tinycomputers.io/posts/cpm-on-physical-retroshield-z80.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;p&gt;There's a profound difference between emulation and the real thing. While my &lt;a href="https://tinycomputers.io/posts/cpm-on-retroshield-z80.html"&gt;previous post&lt;/a&gt; covered running CP/M on a software-based Z80 emulator, this post documents the journey of bringing CP/M 2.2 to life on &lt;em&gt;actual&lt;/em&gt; Z80 silicon - a real Zilog Z80 CPU executing real machine code, with 1MB of DRAM and SD card storage for disk images.&lt;/p&gt;
&lt;p&gt;The result? A fully functional CP/M system running Zork, all on an Arduino Mega 2560 acting as the glue between vintage and modern technology.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; margin: 2em 0;"&gt;
&lt;iframe style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" src="https://www.youtube.com/embed/CwZZKyG_W4A" title="CP/M on RetroShield Z80" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/cpm-on-physical-retroshield-z80_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;15 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;h3&gt;The Hardware Stack&lt;/h3&gt;
&lt;p&gt;Building a working CP/M system requires three essential components: a CPU, memory, and storage. Here's what I used:&lt;/p&gt;
&lt;p&gt;&lt;img alt="The complete hardware stack: Arduino Mega 2560, KDRAM2560 DRAM shield, RetroShield Z80, and SD card module" src="https://tinycomputers.io/images/IMG_4196.jpg"&gt;&lt;/p&gt;
&lt;h4&gt;The RetroShield Z80&lt;/h4&gt;
&lt;p&gt;The &lt;a href="https://baud.rs/DuspIW"&gt;RetroShield&lt;/a&gt; from 8bitforce is a clever piece of engineering. It's a shield that holds a real Z80 CPU and lets an Arduino Mega control it cycle-by-cycle. The Arduino provides the clock, handles bus transactions, and emulates peripherals - but the Z80 is doing the actual computation.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Close-up of the RetroShield Z80 with a Zilog Z84C0004PSC CPU - the real silicon that runs CP/M" src="https://tinycomputers.io/images/IMG_4198.jpg"&gt;&lt;/p&gt;
&lt;p&gt;The RetroShield uses nearly every pin on the Arduino Mega:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;th&gt;Arduino Pins&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Address Bus (A0-A15)&lt;/td&gt;
&lt;td&gt;Pins 22-37&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data Bus (D0-D7)&lt;/td&gt;
&lt;td&gt;Pins 42-49&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Control Signals&lt;/td&gt;
&lt;td&gt;Pins 38-41, 50-53&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This pin-hungry design means we need to be creative about adding peripherals.&lt;/p&gt;
&lt;h4&gt;KDRAM2560: 1MB of Dynamic RAM&lt;/h4&gt;
&lt;p&gt;The &lt;a href="https://baud.rs/iJn6Sd"&gt;KDRAM2560&lt;/a&gt; is another 8bitforce product - a DRAM shield that provides a full megabyte of memory to the Arduino Mega. It uses the analog pins (A0-A15) for its interface, leaving digital pins available for other uses.&lt;/p&gt;
&lt;p&gt;Why DRAM instead of SRAM? Cost and density. A megabyte of SRAM would be expensive and physically large. DRAM is cheap but requires periodic refresh to maintain data integrity. The KDRAM2560 library handles this automatically using one of the Arduino's hardware timers.&lt;/p&gt;
&lt;p&gt;For CP/M, we only need 64KB of the available 1MB, but having extra memory opens possibilities for RAM disks or bank switching in future projects.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#define DRAM_REFRESH_USE_TIMER_1&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;&amp;lt;kdram2560.h&amp;gt;&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Initialize DRAM - this also starts the refresh interrupt&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DRAM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Serial&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Serial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"KDRAM2560: OK (1MB DRAM)"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Serial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"KDRAM2560: FAILED!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Halt&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The API is beautifully simple:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;// Read a byte from any address in the 1MB space&lt;/span&gt;
&lt;span class="n"&gt;byte&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DRAM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Write a byte&lt;/span&gt;
&lt;span class="n"&gt;DRAM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Internally, the library handles the complex multiplexed addressing that DRAM requires - splitting the 20-bit address into row and column components, managing RAS/CAS timing, and ensuring refresh cycles happen frequently enough to prevent data loss.&lt;/p&gt;
&lt;h4&gt;Software SPI SD Card&lt;/h4&gt;
&lt;p&gt;Here's where things get interesting. The obvious choice for SD card storage would be the Arduino's hardware SPI on pins 50-53. But look back at that pin table - the RetroShield uses pins 50-53 for Z80 control signals!&lt;/p&gt;
&lt;p&gt;The solution is software SPI - bit-banging the SPI protocol on different pins. I chose pins 4-7, safely away from both the RetroShield and KDRAM2560:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SD Card Pin&lt;/th&gt;
&lt;th&gt;Arduino Pin&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MISO&lt;/td&gt;
&lt;td&gt;Pin 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MOSI&lt;/td&gt;
&lt;td&gt;Pin 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SCK&lt;/td&gt;
&lt;td&gt;Pin 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CS&lt;/td&gt;
&lt;td&gt;Pin 7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img alt="The SD card module connected via rainbow ribbon cable to the KDRAM2560's prototyping area" src="https://tinycomputers.io/images/IMG_4197.jpg"&gt;&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://baud.rs/W74CwQ"&gt;SdFat library&lt;/a&gt; supports software SPI through its &lt;code&gt;SoftSpiDriver&lt;/code&gt; template class. One important note: you must set &lt;code&gt;SPI_DRIVER_SELECT&lt;/code&gt; to &lt;code&gt;2&lt;/code&gt; in &lt;code&gt;SdFatConfig.h&lt;/code&gt; to enable this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;// In SdFat/src/SdFatConfig.h&lt;/span&gt;
&lt;span class="cp"&gt;#define SPI_DRIVER_SELECT 2  &lt;/span&gt;&lt;span class="c1"&gt;// Enable software SPI&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then in your sketch:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;"SdFat.h"&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SOFT_MISO_PIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SOFT_MOSI_PIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SOFT_SCK_PIN&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SD_CS_PIN&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;SoftSpiDriver&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SOFT_MISO_PIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SOFT_MOSI_PIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SOFT_SCK_PIN&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;softSpi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cp"&gt;#define SD_CONFIG SdSpiConfig(SD_CS_PIN, DEDICATED_SPI, SD_SCK_MHZ(0), &amp;amp;softSpi)&lt;/span&gt;

&lt;span class="n"&gt;SdFs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SD_CONFIG&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Serial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SD Card: OK (Software SPI)"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Software SPI is slower than hardware SPI - roughly 20-50 KB/s compared to 1-2 MB/s. For loading programs at boot and occasional disk access, this is perfectly acceptable. You won't notice the difference playing Zork.&lt;/p&gt;
&lt;h3&gt;The Complete System Architecture&lt;/h3&gt;
&lt;p&gt;Here's how all the pieces fit together:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="err"&gt;┌─────────────────────────────────────────────────────────────┐&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;                      &lt;/span&gt;&lt;span class="nx"&gt;Arduino&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Mega&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2560&lt;/span&gt;&lt;span class="w"&gt;                      &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;├─────────────────────────────────────────────────────────────┤&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;Z80&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;RetroShield&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;layer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;                                &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;Real&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Zilog&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Z80&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;CPU&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="nx"&gt;kHz&lt;/span&gt;&lt;span class="w"&gt;                             &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pins&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;directly&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mapped&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pins&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;49&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;directly&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mapped&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;                       &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;Control&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pins&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;53&lt;/span&gt;&lt;span class="w"&gt;                               &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;├─────────────────────────────────────────────────────────────┤&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;KDRAM2560&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;middle&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;layer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;                                   &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;MB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;DRAM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;via&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;analog&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pins&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;A0&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;A15&lt;/span&gt;&lt;span class="w"&gt;                          &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;Timer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;interrupt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;automatic&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;refresh&lt;/span&gt;&lt;span class="w"&gt;                  &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;Only&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="nx"&gt;KB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;used&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Z80&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;space&lt;/span&gt;&lt;span class="w"&gt;                     &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;├─────────────────────────────────────────────────────────────┤&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;MicroSD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Card&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;external&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;via&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;jumper&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;wires&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;                  &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;Software&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SPI&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pins&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="w"&gt;                                 &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;FAT32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;formatted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;≤&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="nx"&gt;GB&lt;/span&gt;&lt;span class="w"&gt;                                   &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;boot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;CPM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SYS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;A&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DSK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;B&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DSK&lt;/span&gt;&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;└─────────────────────────────────────────────────────────────┘&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Peripheral Emulation: The Arduino's Role&lt;/h3&gt;
&lt;p&gt;While the Z80 executes code, the Arduino handles peripheral I/O. When the Z80 performs an IN or OUT instruction, the Arduino intercepts it and provides the appropriate response.&lt;/p&gt;
&lt;h4&gt;MC6850 ACIA (Serial Console)&lt;/h4&gt;
&lt;p&gt;The console uses a virtual MC6850 ACIA on I/O ports 0x80 (control/status) and 0x81 (data). This connects to the Arduino's Serial interface, which in turn connects to your terminal:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#define ADDR_6850_CONTROL     0x80&lt;/span&gt;
&lt;span class="cp"&gt;#define ADDR_6850_DATA        0x81&lt;/span&gt;

&lt;span class="c1"&gt;// In the I/O read handler:&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ADDR_L&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ADDR_6850_DATA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Z80 is reading from serial&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;prevDATA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Serial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ADDR_L&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ADDR_6850_CONTROL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Z80 is checking status&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Bit 0: Receive data ready&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Bit 1: Transmit buffer empty&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;prevDATA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reg6850_STATUS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// In the I/O write handler:&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ADDR_L&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ADDR_6850_DATA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Z80 is writing to serial&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Serial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DATA_IN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;SD Card Interface&lt;/h4&gt;
&lt;p&gt;The SD card interface uses ports 0x10-0x19, providing commands for file operations and DMA block transfers:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0x10&lt;/td&gt;
&lt;td&gt;Command register&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x11&lt;/td&gt;
&lt;td&gt;Status register&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x12&lt;/td&gt;
&lt;td&gt;Data byte (single-byte I/O)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x13&lt;/td&gt;
&lt;td&gt;Filename character input&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x14-0x15, 0x19&lt;/td&gt;
&lt;td&gt;Seek position (24-bit)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x16-0x17&lt;/td&gt;
&lt;td&gt;DMA address (16-bit)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x18&lt;/td&gt;
&lt;td&gt;Block command (0=read, 1=write)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The key innovation is the DMA block transfer. Instead of the Z80 reading 128 bytes one at a time through port 0x12 (which would require 128 IN instructions), it sets a DMA address and issues a single block command. The Arduino then copies 128 bytes directly between the SD card and DRAM:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;sd_do_block_read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;sdFile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Copy directly to DRAM&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;DRAM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write8&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;unsigned&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;sdDmaAddr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;sdBlockStatus&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Success&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This makes disk operations reasonably fast despite the software SPI limitation.&lt;/p&gt;
&lt;h3&gt;The Boot Process&lt;/h3&gt;
&lt;p&gt;When the Arduino powers up, here's what happens:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Arduino Setup&lt;/li&gt;
&lt;li&gt;Initialize Serial at 115200 baud&lt;/li&gt;
&lt;li&gt;Initialize KDRAM2560 (starts refresh interrupt)&lt;/li&gt;
&lt;li&gt;Initialize SD card via software SPI&lt;/li&gt;
&lt;li&gt;Load &lt;code&gt;boot.bin&lt;/code&gt; from SD card into DRAM at address 0x0000&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Release Z80 from reset&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Z80 Boot Loader (&lt;code&gt;boot.bin&lt;/code&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;Initialize the MC6850 ACIA&lt;/li&gt;
&lt;li&gt;Print boot banner&lt;/li&gt;
&lt;li&gt;Open &lt;code&gt;CPM.SYS&lt;/code&gt; from SD card&lt;/li&gt;
&lt;li&gt;Load it into DRAM at 0xE000 (53 sectors = 6,784 bytes)&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Jump to BIOS cold start at 0xF600&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CP/M BIOS Cold Start&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;Initialize disk variables&lt;/li&gt;
&lt;li&gt;Set up page zero jump vectors&lt;/li&gt;
&lt;li&gt;Print the welcome message&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Jump to CCP (Console Command Processor)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;You see the &lt;code&gt;A&amp;gt;&lt;/code&gt; prompt!&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The boot loader is about 330 bytes of Z80 assembly:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="o"&gt;========================================================================&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Boot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RetroShield&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Z80&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="o"&gt;========================================================================&lt;/span&gt;

&lt;span class="n"&gt;CCP_BASE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="n"&gt;equ&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="mh"&gt;0xE000&lt;/span&gt;
&lt;span class="n"&gt;BIOS_BASE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;equ&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="mh"&gt;0xF600&lt;/span&gt;
&lt;span class="n"&gt;LOAD_SIZE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;equ&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="mi"&gt;53&lt;/span&gt;&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Sectors&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;load&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="mh"&gt;0x0000&lt;/span&gt;

&lt;span class="n"&gt;BOOT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;di&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x0400&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Print&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;boot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;hl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;MSG_BOOT&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;PRINT_STR&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CPM&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SYS&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;hl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;FILENAME&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;SD_SEND_NAME&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CMD_OPEN_READ&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SD_CMD&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Load&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;hl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CCP_BASE&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LOAD_SIZE&lt;/span&gt;

&lt;span class="n"&gt;LOAD_LOOP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;bc&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;hl&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DMA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SD_DMA_LO&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SD_DMA_HI&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;byte&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;via&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DMA&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;xor&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SD_BLOCK&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Print&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;progress&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dot&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'.'&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;PRINT_CHAR&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;hl&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;de&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;hl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;de&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;bc&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;djnz&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;LOAD_LOOP&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Jump&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BIOS&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;jp&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;BIOS_BASE&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;CP/M Disk Images&lt;/h3&gt;
&lt;p&gt;CP/M uses a specific disk format based on the 8-inch floppy standard:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;77 tracks&lt;/li&gt;
&lt;li&gt;26 sectors per track&lt;/li&gt;
&lt;li&gt;128 bytes per sector&lt;/li&gt;
&lt;li&gt;256KB total capacity&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first two tracks are reserved for the system (though we load from &lt;code&gt;CPM.SYS&lt;/code&gt; instead). The directory starts at track 2, sector 0 (byte offset 6,656 or 0x1A00).&lt;/p&gt;
&lt;p&gt;I wrote a Python tool to create and manage these disk images:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# Create an empty disk image&lt;/span&gt;
python3&lt;span class="w"&gt; &lt;/span&gt;cpm_disk.py&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;A.DSK

&lt;span class="c1"&gt;# Add a file&lt;/span&gt;
python3&lt;span class="w"&gt; &lt;/span&gt;cpm_disk.py&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;A.DSK&lt;span class="w"&gt; &lt;/span&gt;ZORK1.COM

&lt;span class="c1"&gt;# List files&lt;/span&gt;
python3&lt;span class="w"&gt; &lt;/span&gt;cpm_disk.py&lt;span class="w"&gt; &lt;/span&gt;list&lt;span class="w"&gt; &lt;/span&gt;A.DSK
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The directory entry format is straightforward - 32 bytes per entry:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Offset&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;User number (0xE5 = empty/deleted)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1-8&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Filename (space-padded)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9-11&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Extension (space-padded)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12-15&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Extent info and record count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16-31&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;Block allocation map&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;One gotcha: empty directory entries must be marked with 0xE5, not 0x00. A disk full of zeros will confuse CP/M into thinking it has files with blank names!&lt;/p&gt;
&lt;h3&gt;Loading Classic Software: Zork on Real Hardware&lt;/h3&gt;
&lt;p&gt;With the infrastructure in place, loading classic software is straightforward. I grabbed Zork I, II, and III from the &lt;a href="https://baud.rs/aTrxyB"&gt;cpm-dist repository&lt;/a&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# Add Zork to the A: drive&lt;/span&gt;
python3&lt;span class="w"&gt; &lt;/span&gt;cpm_disk.py&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;A.DSK&lt;span class="w"&gt; &lt;/span&gt;ZORK1.COM
python3&lt;span class="w"&gt; &lt;/span&gt;cpm_disk.py&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;A.DSK&lt;span class="w"&gt; &lt;/span&gt;ZORK1.DAT
python3&lt;span class="w"&gt; &lt;/span&gt;cpm_disk.py&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;A.DSK&lt;span class="w"&gt; &lt;/span&gt;ZORK2.COM
python3&lt;span class="w"&gt; &lt;/span&gt;cpm_disk.py&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;A.DSK&lt;span class="w"&gt; &lt;/span&gt;ZORK2.DAT

&lt;span class="c1"&gt;# Hitchhiker's Guide goes on B:&lt;/span&gt;
python3&lt;span class="w"&gt; &lt;/span&gt;cpm_disk.py&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;B.DSK&lt;span class="w"&gt; &lt;/span&gt;HITCH.COM
python3&lt;span class="w"&gt; &lt;/span&gt;cpm_disk.py&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;B.DSK&lt;span class="w"&gt; &lt;/span&gt;HITCHHIK.DAT
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Copy the disk images to the SD card, insert it into the module, and reset the Arduino:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/arduino-ide-zork.png" alt="Arduino IDE showing Zork I running on CP/M on the RetroShield Z80" class="img-fluid"&gt;&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="o"&gt;======================================&lt;/span&gt;
&lt;span class="n"&gt;RetroShield&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Z80&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.2&lt;/span&gt;
&lt;span class="o"&gt;======================================&lt;/span&gt;

&lt;span class="nl"&gt;KDRAM2560:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;OK&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;1&lt;/span&gt;&lt;span class="n"&gt;MB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DRAM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;SD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;Card:&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;OK&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Software&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SPI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Loading&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;boot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;Loaded&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;331&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DRAM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0&lt;/span&gt;&lt;span class="n"&gt;x0000&lt;/span&gt;
&lt;span class="n"&gt;Starting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Z80&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;

&lt;span class="n"&gt;RetroShield&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Z80&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Boot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Loader&lt;/span&gt;
&lt;span class="n"&gt;Copyright&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;2025&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Alex&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Jokela&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tinycomputers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;

&lt;span class="n"&gt;Loading&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CPM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SYS&lt;/span&gt;&lt;span class="p"&gt;.....................................................&lt;/span&gt;
&lt;span class="n"&gt;Boot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;


&lt;span class="n"&gt;RetroShield&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.2&lt;/span&gt;
&lt;span class="mh"&gt;56&lt;/span&gt;&lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TPA&lt;/span&gt;

&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;DIR&lt;/span&gt;
&lt;span class="nl"&gt;A:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ZORK1&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;COM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ZORK1&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;DAT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ZORK2&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;COM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ZORK2&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;DAT&lt;/span&gt;
&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ZORK1&lt;/span&gt;

&lt;span class="n"&gt;ZORK&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;I:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;The&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Great&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Underground&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Empire&lt;/span&gt;
&lt;span class="n"&gt;Copyright&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;1981&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;1982&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;1983&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Infocom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Inc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;All&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rights&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reserved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;ZORK&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;registered&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;trademark&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Infocom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Inc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;Revision&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;88&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Serial&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;840726&lt;/span&gt;

&lt;span class="n"&gt;West&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;House&lt;/span&gt;
&lt;span class="n"&gt;You&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;are&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;standing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;an&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;west&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;white&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;house&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;boarded&lt;/span&gt;
&lt;span class="n"&gt;front&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;door&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;There&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;small&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mailbox&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;here&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There's something deeply satisfying about this. The Z80 CPU running this game is the same architecture that ran it in 1981. The actual opcodes being executed are identical. We've just swapped floppy drives for SD cards and CRT terminals for USB serial.&lt;/p&gt;
&lt;h3&gt;The CPU Tick Loop: Where It All Comes Together&lt;/h3&gt;
&lt;p&gt;The heart of the system is the &lt;code&gt;cpu_tick()&lt;/code&gt; function - called continuously in &lt;code&gt;loop()&lt;/code&gt;, it handles one Z80 clock cycle:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kr"&gt;inline&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;__attribute__&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;always_inline&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cpu_tick&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Check for serial input&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Serial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;available&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;reg6850_STATUS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x01&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Set RDRF&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;CLK_HIGH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Rising clock edge&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;uP_ADDR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ADDR&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Capture address bus&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Memory access?&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;STATE_MREQ_N&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;STATE_RD_N&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="c1"&gt;// Memory read - get byte from DRAM&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;DATA_DIR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DIR_OUT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;DATA_OUT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DRAM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read8&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;unsigned&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;uP_ADDR&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;STATE_WR_N&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="c1"&gt;// Memory write - store byte to DRAM&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;DRAM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write8&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;unsigned&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;uP_ADDR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DATA_IN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// I/O access?&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;STATE_IORQ_N&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;STATE_RD_N&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;prevIORQ&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="c1"&gt;// I/O read&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;DATA_DIR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DIR_OUT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sd_handles_port&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ADDR_L&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;prevDATA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sd_read_port&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ADDR_L&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ADDR_L&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ADDR_6850_DATA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;prevDATA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Serial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ADDR_L&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ADDR_6850_CONTROL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;prevDATA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reg6850_STATUS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;DATA_OUT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;prevDATA&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;STATE_WR_N&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;prevIORQ&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="c1"&gt;// I/O write&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;DATA_DIR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DIR_IN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sd_handles_port&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ADDR_L&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;sd_write_port&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ADDR_L&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DATA_IN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ADDR_L&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ADDR_6850_DATA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;Serial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DATA_IN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;prevIORQ&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;STATE_IORQ_N&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;CLK_LOW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Falling clock edge&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;DATA_DIR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DIR_IN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This runs at roughly 100kHz - slow by modern standards, but plenty fast for interactive programs. The Z80 was designed for clock speeds of 2-4MHz, so we're running at about 3-5% of original speed. Text adventures don't mind.&lt;/p&gt;
&lt;h3&gt;The BIOS: Hardware Abstraction in 1KB&lt;/h3&gt;
&lt;p&gt;The CP/M BIOS is where the magic happens. It's the only part of CP/M that needs to be written for each new hardware platform. The BDOS and CCP are universal - they work on any machine with a conforming BIOS.&lt;/p&gt;
&lt;p&gt;Our BIOS implements 17 entry points:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;org&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nx"&gt;BIOS_BASE&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0xF600&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;jp&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;BOOT&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Cold&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;boot&lt;/span&gt;
&lt;span class="nx"&gt;WBOOTE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nx"&gt;jp&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;WBOOT&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;03&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Warm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;boot&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;jp&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;CONST&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;06&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Console&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;jp&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;CONIN&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;09&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Console&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;jp&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;CONOUT&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="nx"&gt;C&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Console&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;jp&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;LIST&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="nx"&gt;F&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;List&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;jp&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;PUNCH&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Punch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;jp&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;READER&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Reader&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;jp&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;HOME&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Home&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;disk&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;jp&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;SELDSK&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;B&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Select&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;disk&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;jp&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;SETTRK&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;E&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;track&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;jp&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;SETSEC&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sector&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;jp&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;SETDMA&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;DMA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;jp&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;READ&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sector&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;jp&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;WRITE&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;A&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Write&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sector&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;jp&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;LISTST&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;D&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;List&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;jp&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;SECTRAN&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Sector&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;translate&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The most complex routines are the disk operations. &lt;code&gt;READ&lt;/code&gt; and &lt;code&gt;WRITE&lt;/code&gt; must calculate the byte offset within the disk image from track and sector numbers:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nt"&gt;CALC_OFFSET&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;offset&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;track&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;26&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;sector&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;128&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;hl&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;TRACK&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;de&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;26&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;Sectors&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;per&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;track&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;call&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;MULT16&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;HL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;track&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;26&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;de&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;SECTOR&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;add&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nt"&gt;hl&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;de&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;HL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;track&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;26&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;sector&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;Multiply&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;by&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;128&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;shift&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;left&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;times&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;xor&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;Clear&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;carry&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;byte&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;add&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nt"&gt;hl&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;hl&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nt"&gt;2&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;adc&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;add&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nt"&gt;hl&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;hl&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nt"&gt;4&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;adc&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;continue&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;shifting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;SEEKPOS&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;hl&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;SEEKPOS&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nt"&gt;2&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;24-bit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;result&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;ret&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The Disk Parameter Block (DPB) tells CP/M about our disk geometry:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;DPB&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;defw&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SPT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sectors&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;per&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;track&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;defb&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BSH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shift&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;defb&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BLM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;defb&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;EXM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;extent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;defw&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="mi"&gt;242&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DSM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;defw&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="mi"&gt;63&lt;/span&gt;&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DRM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;defb&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;xC0&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AL0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;allocation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bitmap&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;defb&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="mh"&gt;0x00&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AL1&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;defw&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CKS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;checksum&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;defw&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;OFF&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reserved&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tracks&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;These parameters define a standard 256KB 8-inch floppy format - the same format used by countless CP/M machines in the late 1970s.&lt;/p&gt;
&lt;h3&gt;Understanding CP/M's Memory Model&lt;/h3&gt;
&lt;p&gt;CP/M's memory layout is elegantly simple. The entire operating system fits in the top 8KB of the 64KB address space:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="err"&gt;┌───────────────────────────────────────┐&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0xFFFF&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="n"&gt;BIOS&lt;/span&gt;&lt;span class="w"&gt;                     &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;KB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Hardware&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;abstraction&lt;/span&gt;
&lt;span class="err"&gt;├───────────────────────────────────────┤&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0xF600&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="n"&gt;BDOS&lt;/span&gt;&lt;span class="w"&gt;                     &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="mf"&gt;3.5&lt;/span&gt;&lt;span class="n"&gt;KB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;O&lt;/span&gt;
&lt;span class="err"&gt;├───────────────────────────────────────┤&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0xE800&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="n"&gt;CCP&lt;/span&gt;&lt;span class="w"&gt;                      &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;KB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;processor&lt;/span&gt;
&lt;span class="err"&gt;├───────────────────────────────────────┤&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0xE000&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;                                       &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;                                       &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="n"&gt;TPA&lt;/span&gt;&lt;span class="w"&gt;                      &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="n"&gt;KB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Your&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;programs&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Transient&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Program&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Area&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;                                       &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;                                       &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;├───────────────────────────────────────┤&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x0100&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="n"&gt;Page&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Zero&lt;/span&gt;&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;variables&lt;/span&gt;
&lt;span class="err"&gt;└───────────────────────────────────────┘&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x0000&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Page Zero contains crucial system information:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0x0000-0x0002&lt;/code&gt;: Jump to warm boot&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x0005-0x0007&lt;/code&gt;: Jump to BDOS entry&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x005C&lt;/code&gt;: Default FCB (File Control Block)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x0080&lt;/code&gt;: Default DMA buffer / command tail&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When you type &lt;code&gt;ZORK1&lt;/code&gt; at the command prompt, CP/M loads &lt;code&gt;ZORK1.COM&lt;/code&gt; at address 0x0100 and jumps there. The program has nearly 56KB to work with - a luxurious amount of memory for 1970s software.&lt;/p&gt;
&lt;h3&gt;Debugging Tips&lt;/h3&gt;
&lt;p&gt;Getting CP/M running required extensive debugging. Here are some tips if you're attempting something similar:&lt;/p&gt;
&lt;h4&gt;Enable Debug Output&lt;/h4&gt;
&lt;p&gt;Set &lt;code&gt;outputDEBUG&lt;/code&gt; to 1 in the Arduino sketch to see every I/O operation:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#define outputDEBUG     1&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This prints every port read/write, which is invaluable for tracking down why the BIOS isn't finding files or why sectors are being read from wrong locations.&lt;/p&gt;
&lt;h4&gt;Check Your Directory Format&lt;/h4&gt;
&lt;p&gt;The most common issue I encountered was improperly formatted disk images. Use a hex editor to verify:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Directory starts at offset 0x1A00 (6656 bytes)&lt;/li&gt;
&lt;li&gt;Empty entries have 0xE5 in byte 0, not 0x00&lt;/li&gt;
&lt;li&gt;Filenames are space-padded to 8 characters, extensions to 3&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Verify DMA Addresses&lt;/h4&gt;
&lt;p&gt;If programs load but crash immediately, check that the DMA address is being set correctly. The BIOS must output both low and high bytes:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;ld      a, l
out     (SD_DMA_LO), a
ld      a, h
out     (SD_DMA_HI), a
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Watch for Register Clobbering&lt;/h4&gt;
&lt;p&gt;Z80 subroutine calls don't preserve registers by default. If your SELDSK routine returns garbage, check whether the OPENDISK helper is destroying HL before the return.&lt;/p&gt;
&lt;h3&gt;Performance Considerations&lt;/h3&gt;
&lt;p&gt;The system runs at approximately 100kHz - about 3% of the Z80's original 4MHz speed. This is limited by:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Arduino loop overhead: Each &lt;code&gt;cpu_tick()&lt;/code&gt; call has function call overhead&lt;/li&gt;
&lt;li&gt;DRAM access time: Software-controlled DRAM is slower than dedicated hardware&lt;/li&gt;
&lt;li&gt;Software SPI: Bit-banging SPI adds latency to disk operations&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For interactive programs like text adventures, this is imperceptible. For computation-heavy tasks, you'd notice the slowdown. WordStar feels sluggish but usable; compiling code would test your patience.&lt;/p&gt;
&lt;p&gt;Future optimizations could include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Assembly-optimized cpu_tick() routine&lt;/li&gt;
&lt;li&gt;Hardware SPI with a different pin arrangement&lt;/li&gt;
&lt;li&gt;Overclocking the Arduino (at your own risk)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Challenges and Solutions&lt;/h3&gt;
&lt;h4&gt;Challenge 1: Pin Conflicts&lt;/h4&gt;
&lt;p&gt;The RetroShield claims the hardware SPI pins (50-53). Solution: software SPI on alternate pins. The SdFat library's &lt;code&gt;SoftSpiDriver&lt;/code&gt; template makes this painless.&lt;/p&gt;
&lt;h4&gt;Challenge 2: Memory Refresh&lt;/h4&gt;
&lt;p&gt;DRAM needs refresh every few milliseconds or it loses data. Solution: the KDRAM2560 library uses Timer 1 interrupts to handle this transparently. The refresh happens in the background - you never need to think about it.&lt;/p&gt;
&lt;h4&gt;Challenge 3: Disk Image Format&lt;/h4&gt;
&lt;p&gt;CP/M expects 0xE5 (not 0x00) for empty directory entries. A disk image initialized to all zeros will confuse CP/M into displaying phantom files. Solution: the &lt;code&gt;cpm_disk.py&lt;/code&gt; tool properly initializes the directory.&lt;/p&gt;
&lt;h4&gt;Challenge 4: 24-bit Seek Positions&lt;/h4&gt;
&lt;p&gt;Disk images are 256KB, requiring 18 bits to fully address. My initial 16-bit seek implementation couldn't access sectors past track 51. Solution: added a third seek port (0x19) for bits 16-23.&lt;/p&gt;
&lt;h4&gt;Challenge 5: SELDSK Return Value Bug&lt;/h4&gt;
&lt;p&gt;CP/M's BDOS expects SELDSK to return a pointer to the Disk Parameter Header in HL. My initial code calculated this pointer, then called OPENDISK which clobbered HL. Solution: push/pop HL around the OPENDISK call.&lt;/p&gt;
&lt;h3&gt;Getting Started: Bill of Materials&lt;/h3&gt;
&lt;p&gt;To build your own CP/M machine, you'll need:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Approximate Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://baud.rs/DzXGr4"&gt;Arduino Mega 2560&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;$15-40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://baud.rs/87wbBL"&gt;Z80 RetroShield&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;$35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://baud.rs/OgyMEk"&gt;KDRAM2560&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://baud.rs/pgYRdX"&gt;MicroSD Card Module&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;$5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://baud.rs/SKeYej"&gt;MicroSD Card (≤32GB FAT32)&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;$5-10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://baud.rs/Gxgyl4"&gt;Jumper wires&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;$5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total&lt;/td&gt;
&lt;td&gt;~$100-130&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Files and Resources&lt;/h3&gt;
&lt;p&gt;The complete Arduino sketch and supporting files are available on GitHub:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Arduino Sketch: &lt;a href="https://baud.rs/IiokcC"&gt;kz80_cpm.ino&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Boot Loader Source: &lt;a href="https://baud.rs/wy6nwp"&gt;boot.asm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;BIOS Source: &lt;a href="https://baud.rs/2YNcPe"&gt;bios.asm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Disk Image Tool: &lt;a href="https://baud.rs/UziZKI"&gt;cpm_disk.py&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Required libraries:
- &lt;a href="https://baud.rs/iJn6Sd"&gt;KDRAM2560&lt;/a&gt; - 1MB DRAM library
- &lt;a href="https://baud.rs/W74CwQ"&gt;SdFat&lt;/a&gt; - SD card with software SPI support&lt;/p&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Building this system was a journey through computing history. CP/M's clean architecture - the separation of BIOS, BDOS, and CCP - made it possible to port a 45-year-old operating system to completely alien hardware in a matter of days. For a fascinating look at CP/M in its heyday, check out the &lt;a href="https://baud.rs/VKgSa6"&gt;Computer Chronicles episode on CP/M&lt;/a&gt; from 1984.&lt;/p&gt;
&lt;p&gt;The Z80 doesn't know it's being fed clock pulses by an Arduino, that its memory is dynamic RAM on a shield, or that its "floppy drives" are files on an SD card. It just executes its opcodes, one after another, exactly as it did in 1978.&lt;/p&gt;
&lt;p&gt;And somewhere in that stream of opcodes, a small mailbox waits west of a white house, just as it has for over four and a half decades.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&amp;gt;open mailbox
Opening the small mailbox reveals a leaflet.

&amp;gt;read leaflet
"WELCOME TO ZORK!

ZORK is a game of adventure, danger, and low cunning. In it you will
explore some of the most amazing territory ever seen by mortals..."
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Welcome to the underground empire. The password is nostalgia, and the treasure is understanding how elegantly simple these early systems really were.&lt;/p&gt;</description><category>arduino</category><category>cp/m</category><category>hardware</category><category>kdram2560</category><category>retro computing</category><category>retroshield</category><category>sd card</category><category>z80</category><guid>https://tinycomputers.io/posts/cpm-on-physical-retroshield-z80.html</guid><pubDate>Wed, 07 Jan 2026 22:00:00 GMT</pubDate></item><item><title>Running CP/M 2.2 on the RetroShield Z80 Emulator</title><link>https://tinycomputers.io/posts/cpm-on-retroshield-z80.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;p&gt;There's something magical about watching a 45-year-old operating system boot on modern hardware. CP/M 2.2, the operating system that launched a thousand microcomputers and paved the way for MS-DOS, still has lessons to teach us about elegant system design.&lt;/p&gt;
&lt;p&gt;This post documents my journey getting CP/M 2.2 running on the &lt;a href="https://baud.rs/2uKnpv"&gt;RetroShield Z80 emulator&lt;/a&gt;, a &lt;a href="https://baud.rs/R1fDfb"&gt;Rust&lt;/a&gt;-based Z80 emulator I've been developing. The result is a fully functional CP/M system that can run classic software like Zork and WordStar.&lt;/p&gt;
&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/cpm-on-retroshield-z80_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;7 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;h3&gt;What is CP/M?&lt;/h3&gt;
&lt;p&gt;CP/M (Control Program for Microcomputers) was created by &lt;a href="https://baud.rs/sv0xWH"&gt;Gary Kildall&lt;/a&gt; at Digital Research in 1974. It became the dominant operating system for &lt;a href="https://baud.rs/BttXOW"&gt;8-bit microcomputers&lt;/a&gt; in the late 1970s and early 1980s, running on machines like the Altair 8800, IMSAI 8080, Osborne 1, and Kaypro.&lt;/p&gt;
&lt;p&gt;CP/M's genius was its portability. The system separated into three layers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CCP&lt;/strong&gt; (Console Command Processor) - The command line interface&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BDOS&lt;/strong&gt; (Basic Disk Operating System) - File and I/O services&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BIOS&lt;/strong&gt; (Basic Input/Output System) - Hardware abstraction&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Only the BIOS needed to be rewritten for each machine. This architecture directly influenced MS-DOS and, by extension, every PC operating system that followed.&lt;/p&gt;
&lt;h3&gt;The RetroShield Z80 Emulator&lt;/h3&gt;
&lt;p&gt;The &lt;a href="https://baud.rs/gpbDmS"&gt;RetroShield&lt;/a&gt; is a hardware shield that lets you run vintage CPUs on modern microcontrollers. My emulator takes this concept further by providing a complete software simulation of the Z80 and its peripherals.&lt;/p&gt;
&lt;p&gt;The emulator includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Full Z80 CPU emulation (via the &lt;code&gt;rz80&lt;/code&gt; crate)&lt;/li&gt;
&lt;li&gt;MC6850 ACIA serial port (console I/O)&lt;/li&gt;
&lt;li&gt;SD card emulation with DMA block transfers&lt;/li&gt;
&lt;li&gt;TUI debugger with memory viewer, disassembly, and single-stepping&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The Challenge: Disk I/O&lt;/h3&gt;
&lt;p&gt;Getting CP/M's console I/O working was straightforward. The real challenge was disk I/O. CP/M expects to read and write 128-byte sectors from floppy disks. I needed to emulate this using files on the host system.&lt;/p&gt;
&lt;p&gt;The standard 8" single-sided, single-density floppy format that CP/M uses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;77 tracks&lt;/li&gt;
&lt;li&gt;26 sectors per track&lt;/li&gt;
&lt;li&gt;128 bytes per sector&lt;/li&gt;
&lt;li&gt;256KB total capacity&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;DMA Block Transfers&lt;/h4&gt;
&lt;p&gt;Rather than transferring bytes one at a time through I/O ports (which would be painfully slow), I implemented DMA block transfers. The BIOS sets up a DMA address and issues a single command to transfer an entire 128-byte sector:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;DMA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;
&lt;span class="nx"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;hl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DMAADR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;
&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SD_DMA_LO&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;
&lt;span class="nx"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;
&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SD_DMA_HI&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;

&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Issue&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;
&lt;span class="nx"&gt;xor&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;
&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SD_BLOCK&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;

&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Check&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;
&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SD_BLOCK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;ret&lt;/span&gt;&lt;span class="w"&gt;                     &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;A&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;OK&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;On the emulator side, this triggers a direct memory copy from the disk image file into emulated RAM.&lt;/p&gt;
&lt;h4&gt;The Bug That Almost Defeated Me&lt;/h4&gt;
&lt;p&gt;After implementing everything, CP/M would boot and print its banner, but then hang or show garbage. The debug output revealed the BDOS was requesting insane track numbers like 0x0083 instead of track 2.&lt;/p&gt;
&lt;p&gt;The culprit? A classic use-after-move bug in the BIOS:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nx"&gt;SELDSK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Calculate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;DPH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;HL&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nx"&gt;hl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hl&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nx"&gt;hl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hl&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nx"&gt;hl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hl&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nx"&gt;hl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hl&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;de&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;DPH0&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nx"&gt;hl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;de&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;OPENDISK&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;BUG&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;This&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;overwrites&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;HL&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;ret&lt;/span&gt;&lt;span class="w"&gt;                     &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Returns&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;garbage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;instead&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;DPH&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;OPENDISK&lt;/code&gt; subroutine was using HL internally, destroying the Disk Parameter Header address that SELDSK was supposed to return. The BDOS would then read garbage from the wrong memory location for its disk parameters.&lt;/p&gt;
&lt;p&gt;The fix was simple:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;hl&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;OPENDISK&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;pop&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nx"&gt;hl&lt;/span&gt;&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Restore&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;DPH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;ret&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;24-bit Seek Positions&lt;/h4&gt;
&lt;p&gt;Another issue: the disk images are 256KB, but I initially only supported 16-bit seek positions (64KB max). I added an extended seek port for the high byte:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SD_SEEK_LO&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;u8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x14&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Bits 0-7&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SD_SEEK_HI&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;u8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x15&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Bits 8-15&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SD_SEEK_EX&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;u8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x19&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Bits 16-23&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;The Memory Map&lt;/h3&gt;
&lt;p&gt;CP/M's memory layout for a 56KB TPA (Transient Program Area):&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="mi"&gt;0000&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;FF&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;Page&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Zero&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jump&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;vectors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;FCB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;0100&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;DFFF&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;TPA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;programs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;load&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;here&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="n"&gt;KB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;E000&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;E7FF&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;CCP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Processor&lt;/span&gt;
&lt;span class="n"&gt;E800&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;F5FF&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;BDOS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Basic&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Disk&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Operating&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;
&lt;span class="n"&gt;F600&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;FFFF&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;BIOS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Hardware&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;abstraction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;layer&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The BIOS is only about 1KB of &lt;a href="https://baud.rs/EsBekO"&gt;Z80 assembly&lt;/a&gt;, handling:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Console I/O via the MC6850 ACIA&lt;/li&gt;
&lt;li&gt;Disk I/O via SD card emulation&lt;/li&gt;
&lt;li&gt;Drive selection and track/sector positioning&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Running Classic Software&lt;/h3&gt;
&lt;p&gt;With CP/M booting successfully, I could run classic software:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Zork I&lt;/strong&gt; - Infocom's legendary text adventure runs perfectly:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/zork.png" alt="Zork I running on CP/M in the RetroShield TUI emulator" class="img-fluid"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;WordStar 3.3&lt;/strong&gt; and &lt;strong&gt;SuperCalc&lt;/strong&gt; also run, though they need terminal escape codes configured properly (the Kaypro version uses ADM-3A codes).&lt;/p&gt;
&lt;h3&gt;Try It Yourself&lt;/h3&gt;
&lt;p&gt;The code is available on GitHub:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://baud.rs/2uKnpv"&gt;RetroShield Z80 Emulator&lt;/a&gt; - The Rust emulator with SD card DMA support&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/mrxmpi"&gt;RetroShield CP/M&lt;/a&gt; - BIOS, boot loader, and CP/M system files&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To run:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;emulator/rust
cargo&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;--release
./target/release/retroshield_tui&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;storage&lt;span class="w"&gt; &lt;/span&gt;path/to/boot.bin
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Press F5 to run, then type &lt;code&gt;zork1&lt;/code&gt; at the &lt;code&gt;A&amp;gt;&lt;/code&gt; prompt.&lt;/p&gt;
&lt;h3&gt;Lessons Learned&lt;/h3&gt;
&lt;p&gt;Building this system reinforced some timeless principles:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Abstraction layers work.&lt;/strong&gt; CP/M's BIOS/BDOS/CCP split made porting trivial. Only 1KB of code needed to be written for a completely new "hardware" platform.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Debug output is essential.&lt;/strong&gt; Adding hex dumps of track/sector values immediately revealed the SELDSK bug.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Read the documentation.&lt;/strong&gt; The &lt;a href="https://tinycomputers.io/data/CPM_2.2_Alteration_Guide_1979.pdf"&gt;CP/M 2.2 System Alteration Guide&lt;/a&gt; is remarkably well-written and explained exactly what the BIOS functions needed to do.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Old code still runs.&lt;/strong&gt; With the right emulation layer, 45-year-old binaries execute flawlessly. The &lt;a href="https://baud.rs/EsBekO"&gt;Z80 instruction set&lt;/a&gt; is eternal.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;There's a certain satisfaction in seeing that &lt;code&gt;A&amp;gt;&lt;/code&gt; prompt appear. It's the same prompt that greeted users in 1977, now running on code I wrote in 2025. The machines change, but the software endures.&lt;/p&gt;</description><category>cp/m</category><category>emulator</category><category>retro computing</category><category>retroshield</category><category>rust</category><category>z80</category><guid>https://tinycomputers.io/posts/cpm-on-retroshield-z80.html</guid><pubDate>Wed, 31 Dec 2025 16:00:00 GMT</pubDate></item><item><title>Building Language Compilers for the Z80: An Anthology of Retrocomputing Languages</title><link>https://tinycomputers.io/posts/building-language-compilers-for-the-z80.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;p&gt;Over the past year, I have been building a collection of programming language compilers and interpreters targeting the venerable &lt;a href="https://baud.rs/baNJFu"&gt;Zilog&lt;/a&gt; Z80 microprocessor. What started as an experiment in retrocomputing has grown into a comprehensive suite of tools spanning multiple programming paradigms: from the functional elegance of &lt;a href="https://baud.rs/y382JR"&gt;LISP&lt;/a&gt; to the object-oriented messaging of Smalltalk, from the structured programming of Pascal and Fortran to the low-level control of C. This anthology documents the common architectural patterns, the unique challenges of targeting an 8-bit processor, and the unexpected joys of bringing modern language implementations to &lt;a href="https://baud.rs/BttXOW"&gt;1970s hardware&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My fascination with the Z80 began in the mid-1990s when I got my first &lt;a href="https://baud.rs/r8VKdN"&gt;TI-85 graphing calculator&lt;/a&gt;. That unassuming device, marketed for algebra and calculus homework, contained a Z80 running at 6 MHz with 28KB of RAM. Discovering that I could write programs in &lt;a href="https://baud.rs/YBGBeN"&gt;Z80 assembly&lt;/a&gt; and run them on this pocket computer was revelatory. I accumulated a small library of Z80 assembly books and spent countless hours learning the instruction set, writing simple games, and understanding how software meets hardware at the most fundamental level. Three decades later, this project represents a return to that formative obsession, now armed with modern tools and a deeper understanding of language implementation.&lt;/p&gt;
&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/building-language-compilers-for-the-z80_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;38 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;h3&gt;The RetroShield Platform&lt;/h3&gt;
&lt;p&gt;The &lt;a href="https://baud.rs/nCcM1G"&gt;RetroShield&lt;/a&gt; is a family of hardware adapters that bridge vintage microprocessors to modern Arduino development boards. The product line covers a remarkable range of classic CPUs: the MOS 6502 (powering the Apple II and Commodore 64), the Motorola 6809 (used in the TRS-80 Color Computer), the Intel 8085, the SC/MP, and the Zilog Z80. Each variant allows the original processor to execute real machine code while the Arduino emulates memory, peripherals, and I/O.&lt;/p&gt;
&lt;p&gt;For this project, I focused exclusively on the RetroShield Z80. The Z80's rich instruction set, hardware BCD support via the DAA instruction, and historical significance as the CPU behind CP/M made it an ideal target for language implementation experiments. The RetroShield Z80 connects the actual Z80 chip to an &lt;a href="https://baud.rs/Fme6fv"&gt;Arduino Mega&lt;/a&gt; (or &lt;a href="https://baud.rs/Fcdj3p"&gt;Teensy&lt;/a&gt; adapter for projects requiring more RAM), which emulates the memory and peripheral chips. This arrangement provides the authenticity of running on actual Z80 silicon while offering the convenience of modern development workflows.  &lt;/p&gt;
&lt;p&gt;The standard memory map provides 8KB of ROM at addresses 0x0000-0x1FFF and 6KB of RAM at 0x2000-0x37FF, though the Teensy adapter expands this significantly to 256KB. Serial I/O is handled through an emulated &lt;a href="https://baud.rs/Cg3FPN"&gt;MC6850 ACIA&lt;/a&gt; chip at ports 0x80 and 0x81, providing the familiar RS-232 interface that connects these vintage programs to modern terminals.&lt;/p&gt;
&lt;p&gt;It needs to be mentioned that if you do have a Z80 RetroShield and you want to run the binaries produced by the compilers collections on actual hardware, you will need a couple things:  1) &lt;a href="https://baud.rs/qXWATa"&gt;bin2c&lt;/a&gt;, this is a program that will take a Z80 binary and turn it into a &lt;code&gt;PROGMEM&lt;/code&gt; statement that you can put into an Arduino sketch. 2) Look at &lt;a href="https://baud.rs/fsS5wM"&gt;this sketch&lt;/a&gt; - there is code in there for emulating the MC6850 ACIA.&lt;/p&gt;
&lt;h3&gt;Common Compiler Architecture: Lexer, Parser, AST, Codegen&lt;/h3&gt;
&lt;p&gt;Every compiler in this collection follows a similar multi-stage architecture, a pattern that has proven itself across decades of compiler construction. Understanding this common structure reveals how the same fundamental approach can target vastly different source languages while producing efficient Z80 machine code.&lt;/p&gt;
&lt;h4&gt;The Lexer: Breaking Text into Tokens&lt;/h4&gt;
&lt;p&gt;The lexer (or tokenizer) is the first stage of compilation, responsible for transforming raw source code into a stream of tokens. Each language has its own lexical grammar: LISP recognizes parentheses and symbols, C identifies keywords and operators, &lt;a href="https://baud.rs/1RmGHE"&gt;Smalltalk&lt;/a&gt; distinguishes between message selectors and literals. Despite these differences, every lexer performs the same fundamental task of categorizing input characters into meaningful units.&lt;/p&gt;
&lt;p&gt;In our Rust implementations, the lexer typically maintains a position in the source string and provides a &lt;code&gt;next_token()&lt;/code&gt; method that advances through the input. This produces tokens like &lt;code&gt;Token::Integer(42)&lt;/code&gt;, &lt;code&gt;Token::Plus&lt;/code&gt;, or &lt;code&gt;Token::Identifier("factorial")&lt;/code&gt;. The lexer handles the tedious work of skipping whitespace, recognizing multi-character operators, and converting digit sequences into numbers.&lt;/p&gt;
&lt;h4&gt;The Parser: Building the Abstract Syntax Tree&lt;/h4&gt;
&lt;p&gt;The parser consumes the token stream and constructs an Abstract Syntax Tree (AST) that represents the hierarchical structure of the program. Most of our compilers use &lt;a href="https://baud.rs/JhMFPU"&gt;recursive descent parsing&lt;/a&gt;, a technique where each grammar rule becomes a function that may call other rule functions. This approach is intuitive, produces readable code, and handles the grammars of most programming languages effectively.&lt;/p&gt;
&lt;p&gt;For example, parsing an arithmetic expression like &lt;code&gt;3 + 4 * 5&lt;/code&gt; requires understanding operator precedence. The parser might have functions like &lt;code&gt;parse_expression()&lt;/code&gt;, &lt;code&gt;parse_term()&lt;/code&gt;, and &lt;code&gt;parse_factor()&lt;/code&gt;, each handling operators at different precedence levels. The result is an AST where the multiplication is grouped as a subtree, correctly representing that it should be evaluated before the addition.&lt;/p&gt;
&lt;h4&gt;Code Generation: Emitting Z80 Machine Code&lt;/h4&gt;
&lt;p&gt;The code generator walks the AST and emits Z80 machine code. This is where the rubber meets the road: abstract operations like "add two numbers" become concrete sequences of Z80 instructions like &lt;code&gt;LD A,(HL)&lt;/code&gt;, &lt;code&gt;ADD A,E&lt;/code&gt;, and &lt;code&gt;LD (DE),A&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Most of our compilers generate code directly into a byte buffer, manually encoding each instruction's opcode and operands. This approach, while requiring intimate knowledge of the Z80 instruction set, gives us precise control over the generated code and avoids the complexity of an intermediate representation or separate assembler pass.&lt;/p&gt;
&lt;h3&gt;The DAA Instruction and BCD Arithmetic&lt;/h3&gt;
&lt;p&gt;One of the most fascinating aspects of Z80 programming is the DAA (Decimal Adjust Accumulator) instruction, opcode 0x27. This single instruction makes the Z80 surprisingly capable at decimal arithmetic, which proves essential for implementing numeric types on an 8-bit processor.&lt;/p&gt;
&lt;h4&gt;What is BCD?&lt;/h4&gt;
&lt;p&gt;Binary Coded Decimal (BCD) is a numeric representation where each decimal digit is stored in 4 bits (a nibble). Rather than storing the number 42 as binary 00101010 (its true binary representation), BCD stores it as 0100 0010, with the first nibble representing 4 and the second representing 2. This "packed BCD" format stores two decimal digits per byte.&lt;/p&gt;
&lt;p&gt;While BCD is less space-efficient than pure binary (you can only represent 0-99 in a byte rather than 0-255), it has a crucial advantage: decimal arithmetic produces exact decimal results without rounding errors. This is why BCD was the standard for financial calculations on mainframes and why pocket calculators (including the famous TI series) used BCD internally.&lt;/p&gt;
&lt;h4&gt;How DAA Works&lt;/h4&gt;
&lt;p&gt;When you perform binary addition on two BCD digits, the result may not be valid BCD. Adding 0x09 and 0x01 gives 0x0A, but 0x0A is not a valid BCD digit. The DAA instruction corrects this: it examines the result and the half-carry flag (which indicates a carry from bit 3 to bit 4, i.e., from the low nibble to the high nibble) and adds 0x06 to any nibble that exceeds 9. After DAA, that 0x0A becomes 0x10, correctly representing decimal 10 in BCD.&lt;/p&gt;
&lt;p&gt;This process works for both addition (after ADD or ADC instructions) and subtraction (after SUB or SBC instructions, where DAA subtracts 0x06 instead of adding it). The Z80 remembers whether the previous operation was addition or subtraction through its N flag.&lt;/p&gt;
&lt;h4&gt;BCD in Our Compilers&lt;/h4&gt;
&lt;p&gt;Several of our compilers use 4-byte packed BCD integers, supporting numbers up to 99,999,999 (8 decimal digits). The addition routine loads bytes from both operands starting from the least significant byte, adds them with ADC (add with carry) to propagate carries between bytes, applies DAA to correct each byte, and stores the result. The entire operation takes perhaps 20 bytes of code but provides exact decimal arithmetic on an 8-bit processor.&lt;/p&gt;
&lt;p&gt;Here is a simplified version of our BCD addition loop:&lt;/p&gt;
&lt;div class="crt-terminal"&gt;bcd_add:
    LD B, 4          ; 4 bytes to process
    OR A             ; Clear carry flag
bcd_add_loop:
    LD A, (DE)       ; Load byte from first operand
    ADC A, (HL)      ; Add byte from second operand with carry
    DAA              ; Decimal adjust
    LD (DE), A       ; Store result
    DEC HL           ; Move to next byte
    DEC DE
    DJNZ bcd_add_loop
    RET&lt;/div&gt;

&lt;p&gt;This pattern appears in kz80_c, kz80_fortran, kz80_smalltalk, and kz80_lisp, demonstrating how a hardware feature designed in 1976 still provides practical benefits for language implementation.&lt;/p&gt;
&lt;h3&gt;The Evolution: From Assembly to C to Rust&lt;/h3&gt;
&lt;p&gt;The journey of implementing these compilers taught us valuable lessons about choosing the right tool for the job, and our approach evolved significantly over time.&lt;/p&gt;
&lt;h4&gt;First Attempt: Pascal in Z80 Assembly&lt;/h4&gt;
&lt;p&gt;Our first language implementation was &lt;a href="https://baud.rs/Fko1v6"&gt;kz80_pascal&lt;/a&gt;, a Pascal interpreter written entirely in Z80 assembly language. This approach seemed natural: if you are targeting the Z80, why not write directly in its native language?&lt;/p&gt;
&lt;p&gt;The reality proved challenging. Z80 assembly, while powerful, is unforgiving. Building a recursive descent parser in assembly requires manually managing the call stack, carefully preserving registers across function calls, and debugging through hex dumps of memory. The resulting interpreter works and provides an interactive REPL for Pascal expressions, but extending it requires significant effort. Every new feature means more assembly, more potential for subtle bugs, and more time spent on implementation details rather than language design.&lt;/p&gt;
&lt;h4&gt;Second Attempt: Fortran 77 in C with SDCC&lt;/h4&gt;
&lt;p&gt;For &lt;a href="https://baud.rs/Vbb4MU"&gt;kz80_fortran&lt;/a&gt;, we tried a different approach: writing the interpreter in C and cross-compiling with SDCC (Small Device C Compiler). This was dramatically more productive. C provided structured control flow, automatic stack management, and the ability to organize code into manageable modules.&lt;/p&gt;
&lt;p&gt;The result is a comprehensive Fortran 77 subset with floating-point arithmetic (via BCD), subroutines and functions, arrays, and block IF statements. The C source compiles to approximately 19KB of Z80 code, fitting comfortably in ROM with room for program storage in RAM.&lt;/p&gt;
&lt;p&gt;However, this approach has limitations. SDCC produces functional but not always optimal code, and debugging requires understanding both the C source and the generated assembly. The interpreter also requires the Teensy adapter with 256KB RAM, as the Arduino Mega's 4KB is insufficient for the runtime data structures.&lt;/p&gt;
&lt;h4&gt;The Rust Workbench: Our Final Form&lt;/h4&gt;
&lt;p&gt;Our breakthrough came with the realization that we did not need the compiler itself to run on the Z80, only the generated code. This insight led to what we call the "&lt;a href="https://baud.rs/R1fDfb"&gt;Rust&lt;/a&gt; workbench" approach: write the compiler in Rust, running on a modern development machine, and have it emit Z80 binary images.&lt;/p&gt;
&lt;p&gt;This architecture provides enormous advantages:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Modern tooling&lt;/strong&gt;: Cargo manages dependencies and builds, rustc catches bugs at compile time, and we have access to the entire Rust ecosystem for testing and development.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fast iteration&lt;/strong&gt;: Compiling a Rust program takes seconds; testing the generated Z80 code in our emulator takes milliseconds. Compare this to the multi-minute flash cycles required when the compiler runs on the target.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Comprehensive testing&lt;/strong&gt;: Each compiler includes both Rust unit tests (testing the lexer, parser, and code generator individually) and integration tests that compile source programs and verify their output in the emulator.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Zero-dependency output&lt;/strong&gt;: Despite being written in Rust, the generated Z80 binaries have no runtime dependencies. They are pure machine code that runs directly on the hardware.&lt;/p&gt;
&lt;p&gt;This approach now powers kz80_lisp, kz80_c, kz80_lua, kz80_smalltalk, kz80_chip8, and retrolang. Each is a standalone Rust binary that reads source code and produces a 32KB ROM image.&lt;/p&gt;
&lt;h3&gt;The Z80 Emulator&lt;/h3&gt;
&lt;p&gt;None of this would be practical without a way to test generated code quickly. Our &lt;a href="https://baud.rs/gZIsn3"&gt;RetroShield Z80 Emulator&lt;/a&gt; provides exactly this: a cycle-accurate Z80 emulation with the same memory map and I/O ports as the real hardware.&lt;/p&gt;
&lt;p&gt;The emulator comes in two versions: a simple passthrough mode (&lt;code&gt;retroshield&lt;/code&gt;) that connects stdin/stdout directly to the emulated serial port, and a full TUI debugger (&lt;code&gt;retroshield_nc&lt;/code&gt;) with register displays, disassembly views, memory inspection, and single-step execution. The passthrough mode enables scripted testing, piping test inputs through the emulator and comparing outputs against expected results. The TUI debugger proves invaluable when tracking down code generation bugs.&lt;/p&gt;
&lt;p&gt;The emulator uses the &lt;a href="https://baud.rs/h7YASx"&gt;superzazu/z80&lt;/a&gt; library for CPU emulation, which provides accurate flag behavior and correct cycle counts. Combined with our MC6850 ACIA emulation, it provides a faithful recreation of the RetroShield environment without requiring physical hardware.&lt;/p&gt;
&lt;h3&gt;Self-Hosting Compilers: LISP and C&lt;/h3&gt;
&lt;p&gt;Two of our compilers achieve something remarkable: they can compile themselves and run on the target hardware. This property, called "self-hosting," is a significant milestone in compiler development.&lt;/p&gt;
&lt;h4&gt;What Does Self-Hosting Mean?&lt;/h4&gt;
&lt;p&gt;A self-hosting compiler is one written in the language it compiles. The classic example is the C compiler: most C compilers are themselves written in C. But this creates a chicken-and-egg problem: how do you compile a C compiler if you need a C compiler to compile it?&lt;/p&gt;
&lt;p&gt;The solution is bootstrapping. You start with a minimal compiler written in some other language (or in machine code), use it to compile a slightly better compiler written in the target language, and iterate until you have a full-featured compiler that can compile its own source code. Once bootstrapped, the compiler becomes self-sustaining: future versions compile themselves.&lt;/p&gt;
&lt;h4&gt;kz80_lisp: A Self-Hosted LISP Compiler&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://baud.rs/hAxvQx"&gt;kz80_lisp&lt;/a&gt; (&lt;a href="https://baud.rs/TE0V9b"&gt;crates.io&lt;/a&gt;) includes a LISP-to-Z80 compiler written in LISP itself. The &lt;code&gt;compiler.lisp&lt;/code&gt; file defines functions that traverse LISP expressions and emit Z80 machine code bytes directly into memory. When you call &lt;code&gt;(COMPILE '(+ 1 2))&lt;/code&gt;, it generates the actual Z80 instructions to load 1 and 2 and add them.&lt;/p&gt;
&lt;p&gt;The self-hosted compiler supports arithmetic expressions, nested function calls, and can generate code that interfaces with the runtime's I/O primitives. While not a full replacement for the Rust-based code generator, it demonstrates that LISP is expressive enough to describe its own compilation to machine code.&lt;/p&gt;
&lt;h4&gt;kz80_c: A Self-Hosted C Compiler&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://baud.rs/NOITa0"&gt;kz80_c&lt;/a&gt; (&lt;a href="https://baud.rs/F5DIts"&gt;crates.io&lt;/a&gt;) goes further: its &lt;code&gt;self/cc.c&lt;/code&gt; file is a complete C compiler written in the C subset it compiles. This compiler reads C source from stdin and outputs Z80 binary to stdout, making it usable in shell pipelines:&lt;/p&gt;
&lt;div class="crt-terminal"&gt;# printf 'int main() { puts("Hello!"); return 0; }\x00' | \
    retroshield self/cc.bin &amp;gt; hello.bin
# retroshield hello.bin
Hello!&lt;/div&gt;

&lt;p&gt;The self-hosted C compiler supports all arithmetic operators, pointers, arrays, global variables, control flow statements, and recursive functions. Its main limitation is memory: the compiler source is approximately 66KB, exceeding the 8KB input buffer available on the Z80. This is a fundamental hardware constraint, not a compiler bug. In theory, a "stage 0" minimal compiler could bootstrap larger compilers.&lt;/p&gt;
&lt;h4&gt;Why Self-Hosting Matters&lt;/h4&gt;
&lt;p&gt;Self-hosting is more than a technical achievement; it validates the language implementation. If the compiler can compile itself correctly, it demonstrates that the language is expressive enough for real programs and that the code generator produces working code under complex conditions. For our Z80 compilers, self-hosting also connects us to the history of computing: the original Small-C compiler by Ron Cain in 1980 was similarly self-hosted on Z80/CP-M systems.&lt;/p&gt;
&lt;h3&gt;The Language Implementations&lt;/h3&gt;
&lt;h4&gt;kz80_lisp&lt;/h4&gt;
&lt;p&gt;A minimal LISP interpreter and compiler featuring the full suite of list operations (CAR, CDR, CONS), special forms (QUOTE, IF, COND, LAMBDA, DEFINE), and recursive function support. The implementation includes a pure-LISP floating-point library and the self-hosted compiler mentioned above.&lt;/p&gt;
&lt;div class="crt-terminal"&gt;kz80_lisp v0.1
&amp;gt; (+ 21 21)
42
&amp;gt; (DEFINE (SQUARE X) (* X X))
SQUARE
&amp;gt; (SQUARE 7)
49&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://baud.rs/hAxvQx"&gt;https://github.com/ajokela/kz80_lisp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Crates.io&lt;/strong&gt;: &lt;a href="https://baud.rs/TE0V9b"&gt;https://crates.io/crates/kz80_lisp&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;kz80_c&lt;/h4&gt;
&lt;p&gt;A C compiler supporting char (8-bit), int (16-bit), float (BCD), pointers, arrays, structs, and a preprocessor with &lt;code&gt;#define&lt;/code&gt; and &lt;code&gt;#include&lt;/code&gt;. The runtime library provides serial I/O and comprehensive BCD arithmetic functions. The self-hosted variant can compile and run C programs entirely on the Z80.&lt;/p&gt;
&lt;div class="crt-terminal"&gt;# cat fibonacci.c
int fib(int n) {
    if (n &amp;lt;= 1) return n;
    return fib(n-1) + fib(n-2);
}
int main() {
    puts("Fibonacci:");
    for (int i = 0; i &amp;lt; 10; i = i + 1)
        print_num(fib(i));
    return 0;
}

# kz80_c fibonacci.c -o fib.bin
# retroshield -l fib.bin
Fibonacci:
0 1 1 2 3 5 8 13 21 34&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://baud.rs/NOITa0"&gt;https://github.com/ajokela/kz80_c&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Crates.io&lt;/strong&gt;: &lt;a href="https://baud.rs/F5DIts"&gt;https://crates.io/crates/kz80_c&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;kz80_smalltalk&lt;/h4&gt;
&lt;p&gt;A Smalltalk subset compiler implementing the language's distinctive message-passing syntax with left-to-right operator evaluation. Expressions like &lt;code&gt;1 + 2 * 3&lt;/code&gt; evaluate to 9 (not 7), matching Smalltalk's uniform treatment of binary messages. All arithmetic uses BCD with the DAA instruction.&lt;/p&gt;
&lt;div class="crt-terminal"&gt;# echo "6 * 7" | kz80_smalltalk /dev/stdin -o answer.bin
# retroshield -l answer.bin
Tiny Smalltalk on Z80
42&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://baud.rs/MnVbh7"&gt;https://github.com/ajokela/kz80_smalltalk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Crates.io&lt;/strong&gt;: &lt;a href="https://baud.rs/6ZYKnx"&gt;https://crates.io/crates/kz80_smalltalk&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;kz80_lua&lt;/h4&gt;
&lt;p&gt;A Lua compiler producing standalone ROM images with an embedded virtual machine. Supports tables (Lua's associative arrays), first-class functions, closures, and familiar control structures. The generated VM interprets Lua bytecode, with frequently-used operations implemented in native Z80 code for performance.&lt;/p&gt;
&lt;div class="crt-terminal"&gt;# cat factorial.lua
function factorial(n)
    if n &amp;lt;= 1 then return 1 end
    return n * factorial(n - 1)
end
print("5! =", factorial(5))

# kz80_lua factorial.lua -o fact.bin
# retroshield -l fact.bin
Tiny Lua v0.1
5! =    120&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://baud.rs/F6FQlC"&gt;https://github.com/ajokela/kz80_lua&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Crates.io&lt;/strong&gt;: &lt;a href="https://baud.rs/H5ts26"&gt;https://crates.io/crates/kz80_lua&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;kz80_fortran&lt;/h4&gt;
&lt;p&gt;A &lt;a href="https://baud.rs/teDqSH"&gt;Fortran 77&lt;/a&gt; interpreter with free-format input, REAL numbers via BCD floating point, block IF/THEN/ELSE/ENDIF, DO loops, subroutines, and functions. Requires the Teensy adapter for sufficient RAM. Written in C and cross-compiled with SDCC.&lt;/p&gt;
&lt;div class="crt-terminal"&gt;FORTRAN-77 Interpreter v0.3
RetroShield Z80
Ready.
&amp;gt; INTEGER X, Y
&amp;gt; X = 7
&amp;gt; Y = X * 6
&amp;gt; WRITE(*,*) 'Answer:', Y
Answer: 42&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://baud.rs/Vbb4MU"&gt;https://github.com/ajokela/kz80_fortran&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;kz80_pascal&lt;/h4&gt;
&lt;p&gt;A Pascal interpreter implemented in pure Z80 assembly. Provides an interactive REPL for expression evaluation with integer arithmetic, boolean operations, and comparison operators. A testament to the challenges of assembly language programming.&lt;/p&gt;
&lt;div class="crt-terminal"&gt;Tiny Pascal v0.1
For RetroShield Z80
(Expression Eval Mode)

&amp;gt; 2 + 3 * 4
 = 00014
&amp;gt; TRUE AND (5 &amp;gt; 3)
 = TRUE&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://baud.rs/Fko1v6"&gt;https://github.com/ajokela/kz80_pascal&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;retrolang&lt;/h4&gt;
&lt;p&gt;A custom systems programming language with Pascal/C-like syntax, featuring 16-bit integers, 8-bit bytes, pointers, arrays, inline assembly, and full function support with recursion. Compiles to readable Z80 assembly before assembling to binary.&lt;/p&gt;
&lt;div class="crt-terminal"&gt;# cat squares.rl
proc main()
    var i: int;
    print("Squares: ");
    for i := 1 to 5 do
        printi(i * i);
        printc(32);
    end;
    println();
end;

# retrolang squares.rl --binary -o squares.bin
# retroshield -l squares.bin
Squares: 1 4 9 16 25&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://baud.rs/pX8HqT"&gt;https://github.com/ajokela/retrolang&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Crates.io&lt;/strong&gt;: &lt;a href="https://baud.rs/eoeIuy"&gt;https://crates.io/crates/retrolang&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;kz80_chip8&lt;/h4&gt;
&lt;p&gt;A static recompiler that transforms CHIP-8 programs into native Z80 code. Rather than interpreting CHIP-8 bytecode at runtime, the compiler analyzes each instruction and generates equivalent Z80 sequences. Classic games like Space Invaders and Tetris run directly on the hardware.&lt;/p&gt;
&lt;div class="crt-terminal"&gt;# kz80_chip8 -d ibm_logo.ch8
200: 00E0  CLS
202: A22A  LD   I, 22A
204: 600C  LD   V0, 0C
206: 6108  LD   V1, 08
208: D01F  DRW  V0, V1, 15
20A: 7009  ADD  V0, 09
20C: A239  LD   I, 239
20E: D01F  DRW  V0, V1, 15
...

# kz80_chip8 ibm_logo.ch8 -o ibm.bin
# retroshield -l ibm.bin
CHIP-8 on Z80
[displays IBM logo]&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://baud.rs/VxuBz4"&gt;https://github.com/ajokela/kz80_chip8&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Crates.io&lt;/strong&gt;: &lt;a href="https://baud.rs/IhE82m"&gt;https://crates.io/crates/kz80_chip8&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Why Rust for Compiler Development?&lt;/h3&gt;
&lt;p&gt;The choice of Rust for our compiler workbench was not accidental. Several features make it exceptionally well-suited for this work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Strong typing catches bugs early&lt;/strong&gt;. When you're generating machine code, off-by-one errors or type mismatches can produce binaries that crash or compute wrong results. Rust's type system prevents many such errors at compile time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pattern matching excels at AST manipulation&lt;/strong&gt;. Walking a syntax tree involves matching on node types and recursively processing children. Rust's &lt;code&gt;match&lt;/code&gt; expressions with destructuring make this natural and exhaustive (the compiler warns if you forget a case).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Zero-cost abstractions&lt;/strong&gt;. We can use high-level constructs like iterators, enums with data, and trait objects without runtime overhead. The generated compiler code is as efficient as hand-written C.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Excellent tooling&lt;/strong&gt;. Cargo's test framework made it easy to build comprehensive test suites. Each compiler has dozens to hundreds of tests that run in seconds, providing confidence when making changes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Memory safety without garbage collection&lt;/strong&gt;. This matters less for the compilers themselves (which are desktop tools) but more for our mental model: thinking about ownership and lifetimes transfers naturally to thinking about Z80 register allocation and stack management.&lt;/p&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Building these compilers has been a journey through computing history, from the Z80's 1976 architecture to modern Rust tooling, from the fundamentals of lexing and parsing to the intricacies of self-hosting. The BCD arithmetic that seemed like a curiosity became a practical necessity; the emulator that started as a debugging aid became essential infrastructure; the Rust workbench that felt like an optimization became the key to productivity.&lt;/p&gt;
&lt;p&gt;The Z80 remains a remarkable teaching platform. Its simple instruction set is comprehensible in an afternoon, yet implementing real languages for it requires genuine compiler engineering. Every language in this collection forced us to think carefully about representation, evaluation, and code generation in ways that higher-level targets often obscure.&lt;/p&gt;
&lt;p&gt;All of these projects are open source under BSD-3-Clause licenses. The compilers are available on both GitHub and crates.io, ready to install with &lt;code&gt;cargo install&lt;/code&gt;. Whether you are interested in retrocomputing, compiler construction, or just curious how programming languages work at the metal level, I hope these tools and their source code prove useful.&lt;/p&gt;
&lt;p&gt;The Z80 may be nearly 50 years old, but it still has lessons to teach.&lt;/p&gt;</description><category>bcd</category><category>c</category><category>compilers</category><category>daa</category><category>fortran</category><category>lisp</category><category>lua</category><category>pascal</category><category>retrocomputing</category><category>rust</category><category>smalltalk</category><category>z80</category><guid>https://tinycomputers.io/posts/building-language-compilers-for-the-z80.html</guid><pubDate>Fri, 12 Dec 2025 18:00:00 GMT</pubDate></item></channel></rss>