<?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 wifi)</title><link>https://tinycomputers.io/</link><description></description><atom:link href="https://tinycomputers.io/categories/wifi.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>Mon, 06 Apr 2026 22:12:58 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><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></channel></rss>