<?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 cp/m)</title><link>https://tinycomputers.io/</link><description></description><atom:link href="https://tinycomputers.io/categories/cpm.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:57 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>An LLM Clean Room Z80 Emulator: Building from Specifications, Not Source Code</title><link>https://tinycomputers.io/posts/clean-room-z80-emulator.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/clean-room-z80-emulator_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;43 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;After "Starting Z80...", the Z80 boot loader runs. It opens &lt;code&gt;CPM.SYS&lt;/code&gt; via the sector server, loads the CCP and BDOS into memory at &lt;code&gt;0xE000&lt;/code&gt;, jumps to the BIOS cold boot at &lt;code&gt;0xF600&lt;/code&gt;, and if everything works, prints a banner and the &lt;code&gt;A&amp;gt;&lt;/code&gt; prompt.&lt;/p&gt;
&lt;h3&gt;What's Next&lt;/h3&gt;
&lt;p&gt;The WiFi communication works. The shadow register tracking mechanism works; I've confirmed partial serial output from the Z80 (the ACIA console I/O goes through the same shadow register path). The instruction skip counter prevents refresh cycle confusion. All the pieces are in place.&lt;/p&gt;
&lt;p&gt;What remains is completeness testing. The shadow register tracking needs to cover every instruction the BIOS and CCP actually use. If the Z80 executes an instruction that modifies A through a path we don't track (say, a &lt;code&gt;POP AF&lt;/code&gt; or &lt;code&gt;EX AF, AF'&lt;/code&gt;), the shadow will be wrong and the next &lt;code&gt;OUT&lt;/code&gt; will send garbage. The fix is straightforward (add more cases to the switch statement) but requires methodical testing.&lt;/p&gt;
&lt;p&gt;There's also the 8MB SDRAM sitting unused on the Giga. Once CP/M boots reliably, the obvious next step is downloading entire disk images into SDRAM over WiFi at startup. At that point, all disk I/O becomes memory-mapped, with no network latency and no TCP overhead. CP/M running at memory speed on a RAM disk, served from a real Z80 that thinks it's talking to a floppy drive.&lt;/p&gt;
&lt;p&gt;Once the project is stable and CP/M boots reliably, I plan to open-source the full KiCad PCB design files for the level converter shield, along with the Arduino sketch and sector server. The level converter board uses nine TXB0108PW ICs in TSSOP-20 packages to translate all 72 signals between the Giga's 3.3V and the RetroShield's 5V. It's a straightforward two-layer design that anyone could get fabricated.&lt;/p&gt;
&lt;p&gt;The debugging journey from "IORQ_N is stuck" to "let's just decode the entire instruction stream in software" was not the path I expected to take. But it turned a level converter limitation into something arguably more interesting: a system where the Arduino doesn't just babysit the Z80's bus signals, but understands what the Z80 is thinking.&lt;/p&gt;</description><category>arduino</category><category>arduino giga</category><category>cp/m</category><category>hardware</category><category>level shifter</category><category>retro computing</category><category>retroshield</category><category>rust</category><category>sector server</category><category>stm32</category><category>wifi</category><category>z80</category><guid>https://tinycomputers.io/posts/cpm-on-arduino-giga-r1-wifi.html</guid><pubDate>Sat, 14 Feb 2026 21:00:00 GMT</pubDate></item><item><title>George Morrow: Pioneer of Personal Computing</title><link>https://tinycomputers.io/posts/george-morrow-pioneer-of-personal-computing.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;p&gt;In the pantheon of personal computing pioneers, certain names dominate the narrative: Steve Jobs, Steve Wozniak, Bill Gates. Yet the early microcomputer industry was built by dozens of brilliant engineers and entrepreneurs whose contributions, while perhaps less celebrated, were no less significant. Among these figures stands George C. Morrow, a man described by Richard Dalton in the &lt;em&gt;Whole Earth Software Catalog&lt;/em&gt; as "one of the microcomputer industry's iconoclasts." His journey from high school dropout to computer industry pioneer to jazz preservationist tells a uniquely American story of reinvention, innovation, and passion.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/morrow-md3-micro-decision.jpg" alt="Morrow Micro Decision MD-3 computer" class="img-shadow" style="float: right; max-width: 400px; margin: 0 0 20px 20px;" loading="lazy"&gt;&lt;/p&gt;
&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/george-morrow-pioneer-of-personal-computing_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;21 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;h3&gt;From Detroit to Stanford: An Unconventional Path&lt;/h3&gt;
&lt;p&gt;George Morrow was born on January 30, 1934, in Detroit, Michigan. His early life followed an unconventional trajectory that would later inform his maverick approach to business. Morrow dropped out of high school, a decision that might have closed doors for others. But at the age of 28, driven by intellectual curiosity, he made the remarkable decision to return to education.&lt;/p&gt;
&lt;p&gt;Morrow earned a bachelor's degree in physics from Stanford University, one of the nation's most prestigious institutions. He followed this with a master's degree in mathematics from the University of Oklahoma. Not content to stop there, he pursued doctoral studies in mathematics at the University of California, Berkeley. It was at Berkeley that Morrow's life took its decisive turn. Working as a programmer in the university's computer lab, he became captivated by the emerging world of computing. The pursuit of a PhD in mathematics gave way to a new passion that would define the rest of his career.&lt;/p&gt;
&lt;h3&gt;The Homebrew Computer Club and the Birth of an Industry&lt;/h3&gt;
&lt;p&gt;The year 1975 marked a watershed moment in computing history. The MITS Altair 8800 appeared on the cover of &lt;em&gt;Popular Electronics&lt;/em&gt;, igniting the imagination of hobbyists and engineers across the country. In the San Francisco Bay Area, these enthusiasts began gathering at the Homebrew Computer Club, an informal group that would spawn an extraordinary number of future industry leaders.&lt;/p&gt;
&lt;p&gt;Morrow became an active member of this legendary collective, documented extensively in &lt;a href="https://baud.rs/0ltUMy" target="_blank" rel="noopener"&gt;Fire in the Valley&lt;/a&gt;. Among his fellow members were Steve Wozniak, who would co-found Apple; Harry Garland and Roger Melen of Cromemco; Adam Osborne of Osborne Computer; Lee Felsenstein, designer of the Osborne 1; and Bob Marsh of Processor Technology. The club served as an incubator for ideas, a forum for technical exchange, and a launching pad for commercial ventures.&lt;/p&gt;
&lt;h3&gt;From Morrow's Microstuff to Thinker Toys&lt;/h3&gt;
&lt;p&gt;Starting in 1976, Morrow began designing and selling computer parts and accessories. His first venture was Morrow's Microstuff, which sold memory boards for the Altair 8800 via mail order. His initial product was an Intel 8080 board featuring an octal-notation keypad, but it proved unappealing to hobbyists who preferred the binary notation and flip switches of the Altair itself.&lt;/p&gt;
&lt;p&gt;Undeterred, Morrow pivoted to what would become his signature contribution: storage solutions. He began selling floppy disk drives for S-100 machines, packaging them as complete systems that included an 8-inch external drive, a controller board, the CP/M operating system, and CBASIC. This bundle proved enormously popular, establishing Morrow as a significant player in the nascent industry.&lt;/p&gt;
&lt;p&gt;Morrow rebranded his company as Thinker Toys, a name that captured the playful yet serious nature of the hobbyist computing world. However, the toy company that manufactured Tinkertoys took exception to the similar name and threatened legal action. Morrow was forced to rename his business once more, this time as Morrow Designs, the name by which it would become best known. The company set up operations at 600 McCormick Street in San Leandro, California.&lt;/p&gt;
&lt;h3&gt;Bringing Mainframe Concepts to Microcomputers&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/morrow-md11.jpg" alt="Morrow MD-11 computer system" class="img-shadow" style="float: left; max-width: 350px; margin: 0 20px 20px 0;" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;What set Morrow apart from many competitors was his willingness to bring sophisticated engineering concepts from the mainframe world to microcomputers. The Disk Jockey/DMA (DJDMA) floppy disk controller exemplified this philosophy. As the company's technical documentation explained: "The idea of an intelligent I/O channel was first implemented by IBM on their famous 370 mainframes. Now for the first time, this powerful concept has been implemented on the S100 bus."&lt;/p&gt;
&lt;p&gt;The DJDMA featured its own Z-80 4MHz microprocessor dedicated to supervising data transfers between disk drives and system memory. This "channel concept," borrowed directly from IBM's mainframe architecture, allowed the controller to operate independently of the main CPU. The documentation boasted: "All in all, there is nothing on the S-100 bus in the way of a floppy disk controller that comes anywhere near the performance and versatility of the DJDMA. For that matter, we here at Morrow Designs know of no other floppy disk controller on any bus that can match the DJDMA in price, power, performance, and flexibility."&lt;/p&gt;
&lt;p&gt;This wasn't mere marketing hyperbole. The controller supported both 8-inch and 5¼-inch drives, could accommodate up to eight drives simultaneously, and featured programmable bipolar LSI logic that could read and write media with almost any format. The engineering approach reflected Morrow's belief that microcomputer users deserved the same sophisticated I/O handling that mainframe users enjoyed.&lt;/p&gt;
&lt;p&gt;Morrow Designs also ventured into hard disk storage with products like the Discus M26, a complete mass storage subsystem with a 14-inch Winchester-type hard disk offering 26 million bytes of formatted storage, an enormous capacity for the era. The system could be expanded to 104 million bytes with four drives. The company was already planning cassette tape backup systems for 1980, recognizing the data security challenges posed by non-removable media.&lt;/p&gt;
&lt;h3&gt;The S-100 Bus and IEEE-696: Building Industry Standards&lt;/h3&gt;
&lt;p&gt;One of Morrow's most enduring contributions to computing was his work on the S-100 bus standard. The S-100 bus (named for its 100-pin connector) had originated with the Altair 8800. While it enabled a thriving ecosystem of compatible boards and peripherals, the lack of formal standardization created compatibility problems as manufacturers interpreted the bus specifications differently.&lt;/p&gt;
&lt;p&gt;Morrow recognized that the industry needed rigorous standards to mature. The first symposium on the S-100 bus was held on November 20, 1976, at Diablo Valley College, moderated by Jim Warren with a panel that included Morrow, Harry Garland, and Lee Felsenstein. In July 1979, Morrow joined Kells Elmquist, Howard Fullmer, and David Gustavson in publishing a "Standard Specification for S-100 Bus Interface Devices," extending the data path to 16 bits and the address path to 24 bits.&lt;/p&gt;
&lt;p&gt;The IEEE 696 Working Group continued developing the specification, which was approved by the IEEE Computer Society on June 10, 1982, and by ANSI on September 8, 1983 as IEEE Std 696-1983. Morrow was, by many accounts, the person most responsible for the design, specification, and approval of this standard. His DJDMA documentation reflected this commitment to standards: "A great deal of care has been taken in the design of the interface circuitry so it conforms in every detail to this new standard and still allows the controller to work well with existing systems designed before the standardization effort was started."&lt;/p&gt;
&lt;h3&gt;The Decision 1 and Micro Decision: Computing for Everyone&lt;/h3&gt;
&lt;p&gt;In 1979, George Morrow and his wife Michiko Jean formally incorporated Morrow Designs. The company's product line expanded to include complete computer systems aimed at making business computing affordable.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;Decision 1&lt;/strong&gt; represented the high end of Morrow's offerings. According to the December 1982 price list, the base D1 model sold for \$2,395 and featured an S-100 (IEEE 696) 14-slot motherboard, Z80 CPU, real-time CMOS clock/calendar chip, programmable interrupt controller, three RS232C serial ports, one parallel printer port, and 64K of high-speed static RAM expandable to a full megabyte. The top-of-the-line D3C model, with a 16-megabyte hard disk, commanded \$5,400. The company also offered multi-user upgrade kits with the Micronix operating system, enabling small businesses to share a single computer among multiple terminals.&lt;/p&gt;
&lt;p&gt;But it was the &lt;strong&gt;Micro Decision&lt;/strong&gt; line, introduced in 1982, that best exemplified Morrow's commitment to affordability. The MD1 started at just \$1,195 and included a Z80A CPU running at 4MHz, 64K of RAM, two RS232C serial ports, a floppy disk controller supporting up to four drives, and a complete software bundle: CP/M 2.2, Microsoft BASIC 80, the company's own BaZic interpreter (compatible with NorthStar BASIC), WordStar word processor, LogiCalc spreadsheet, and Correct-It spelling checker.&lt;/p&gt;
&lt;p&gt;The MD2, with two single-sided drives providing 400K of storage, sold for \$1,545. The MD3, featuring two double-sided drives with 768K total storage plus the Personal Pearl database manager, cost \$1,695. Optional terminals (the MDT20 in beige or MDT50 in black) added \$595. These were remarkable prices for complete, software-bundled systems at a time when competing products often cost significantly more.&lt;/p&gt;
&lt;h3&gt;Bankruptcy and Beyond&lt;/h3&gt;
&lt;p&gt;As the IBM PC and its clones came to dominate the market, Morrow Designs faced an existential challenge. The company's CP/M-based systems, however excellent, were increasingly seen as outdated. In 1984, Morrow released the Pivot, an IBM-compatible portable computer, licensing the design to Zenith Electronics Corporation for \$1.2 million.&lt;/p&gt;
&lt;p&gt;The Zenith deal, ironically, hastened the company's demise. In 1986, Zenith won a \$27 million contract with the Internal Revenue Service to supply computers based on Morrow's design. But the terms of the licensing agreement and the company's mounting debts proved unsustainable. On March 11, 1986, just weeks after Zenith won the IRS contract, Morrow Designs filed for bankruptcy.&lt;/p&gt;
&lt;h3&gt;Computer Chronicles and a Second Act&lt;/h3&gt;
&lt;p&gt;Even as his company struggled, Morrow found a new platform for his expertise. He became a co-host of &lt;em&gt;Computer Chronicles&lt;/em&gt;, a public television program that explained computing to general audiences. Morrow appeared in over 40 episodes between 1985 and 1995, sharing his deep knowledge of hardware, software, and industry trends with viewers across the country. His appearances revealed the same qualities that had defined his career: technical depth combined with an ability to communicate complex ideas clearly, and an iconoclastic perspective that questioned industry assumptions.&lt;/p&gt;
&lt;p&gt;Following the collapse of Morrow Designs, George Morrow turned to a passion he had nurtured throughout his life: jazz and big band music from the 1920s and 1930s. He assembled one of the largest private collections of 78 RPM records in the country, eventually exceeding 70,000 items, with a particular focus on rare dance and jazz recordings from the pre-war era.&lt;/p&gt;
&lt;p&gt;Applying the same engineering mindset that had driven his computer work, he developed a sophisticated system to digitize and restore these fragile shellac recordings. His son John recalled that his father would labor for hours or even days over a single recorded song until satisfied with the restored version. Morrow released these painstakingly restored recordings under his own label, Old Masters, preserving an important piece of American musical heritage for future generations.&lt;/p&gt;
&lt;h3&gt;Legacy&lt;/h3&gt;
&lt;p&gt;George Morrow died on May 7, 2003, at his home in San Mateo, California, from complications of aplastic anemia. He was 69 years old. He was survived by his wife Michiko Jean, sons John and William, and daughter Kelly.&lt;/p&gt;
&lt;p&gt;Morrow's contributions to computing extend far beyond the products that bore his name. His work on the IEEE-696 standard helped transform a hobbyist ecosystem into a professional industry. His decision to bring mainframe I/O concepts to microcomputers pushed the entire field forward. His commitment to affordable, software-bundled systems anticipated the value-driven approach that would eventually democratize personal computing.&lt;/p&gt;
&lt;p&gt;The archive of Morrow Designs documentation (including technical manuals, BIOS source code, price lists, and product literature) survives as a resource for historians and retrocomputing enthusiasts. These documents reveal not just the products themselves but the engineering philosophy behind them: a belief that microcomputer users deserved sophisticated, well-documented systems at fair prices.&lt;/p&gt;
&lt;p&gt;Perhaps most importantly, Morrow embodied the spirit of the early microcomputer industry: the conviction that small teams of dedicated engineers could build transformative technologies, that standards and cooperation could coexist with competition, and that computing should be accessible to everyone, not just institutions and corporations.&lt;/p&gt;
&lt;h3&gt;Further Reading&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://baud.rs/0ltUMy" target="_blank" rel="noopener"&gt;Fire in the Valley: The Birth and Death of the Personal Computer&lt;/a&gt; by Michael Swaine and Paul Freiberger. The definitive history of the Homebrew Computer Club and the personal computer revolution.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/Wv1Dnh" target="_blank" rel="noopener"&gt;Hackers: Heroes of the Computer Revolution&lt;/a&gt; by Steven Levy. A classic account of the hacker ethic and the people who built the early computer industry.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/R8hnjV" target="_blank" rel="noopener"&gt;The Soul of a New Machine&lt;/a&gt; by Tracy Kidder. Pulitzer Prize-winning chronicle of a computer engineering team, capturing the intensity of the era.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;</description><category>computer chronicles</category><category>cp/m</category><category>george morrow</category><category>homebrew computer club</category><category>ieee-696</category><category>micro decision</category><category>microcomputer history</category><category>morrow designs</category><category>retrocomputing</category><category>s-100</category><guid>https://tinycomputers.io/posts/george-morrow-pioneer-of-personal-computing.html</guid><pubDate>Fri, 30 Jan 2026 15:00:00 GMT</pubDate></item><item><title>CP/M 2.2 on Physical Hardware: RetroShield Z80 with 1MB DRAM and SD Card Storage</title><link>https://tinycomputers.io/posts/cpm-on-physical-retroshield-z80.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;p&gt;There's a profound difference between emulation and the real thing. While my &lt;a href="https://tinycomputers.io/posts/cpm-on-retroshield-z80.html"&gt;previous post&lt;/a&gt; covered running CP/M on a software-based Z80 emulator, this post documents the journey of bringing CP/M 2.2 to life on &lt;em&gt;actual&lt;/em&gt; Z80 silicon - a real Zilog Z80 CPU executing real machine code, with 1MB of DRAM and SD card storage for disk images.&lt;/p&gt;
&lt;p&gt;The result? A fully functional CP/M system running Zork, all on an Arduino Mega 2560 acting as the glue between vintage and modern technology.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; margin: 2em 0;"&gt;
&lt;iframe style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" src="https://www.youtube.com/embed/CwZZKyG_W4A" title="CP/M on RetroShield Z80" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen&gt;&lt;/iframe&gt;
&lt;/div&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;Press F5 to run, then type &lt;code&gt;zork1&lt;/code&gt; at the &lt;code&gt;A&amp;gt;&lt;/code&gt; prompt.&lt;/p&gt;
&lt;h3&gt;Lessons Learned&lt;/h3&gt;
&lt;p&gt;Building this system reinforced some timeless principles:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Abstraction layers work.&lt;/strong&gt; CP/M's BIOS/BDOS/CCP split made porting trivial. Only 1KB of code needed to be written for a completely new "hardware" platform.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Debug output is essential.&lt;/strong&gt; Adding hex dumps of track/sector values immediately revealed the SELDSK bug.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Read the documentation.&lt;/strong&gt; The &lt;a href="https://tinycomputers.io/data/CPM_2.2_Alteration_Guide_1979.pdf"&gt;CP/M 2.2 System Alteration Guide&lt;/a&gt; is remarkably well-written and explained exactly what the BIOS functions needed to do.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Old code still runs.&lt;/strong&gt; With the right emulation layer, 45-year-old binaries execute flawlessly. The &lt;a href="https://baud.rs/EsBekO"&gt;Z80 instruction set&lt;/a&gt; is eternal.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;There's a certain satisfaction in seeing that &lt;code&gt;A&amp;gt;&lt;/code&gt; prompt appear. It's the same prompt that greeted users in 1977, now running on code I wrote in 2025. The machines change, but the software endures.&lt;/p&gt;</description><category>cp/m</category><category>emulator</category><category>retro computing</category><category>retroshield</category><category>rust</category><category>z80</category><guid>https://tinycomputers.io/posts/cpm-on-retroshield-z80.html</guid><pubDate>Wed, 31 Dec 2025 16:00:00 GMT</pubDate></item><item><title>What VisiCalc Teaches Us About AI: The 45-Year Pattern of "This Time It's Different"</title><link>https://tinycomputers.io/posts/what-visicalc-teaches-us-about-ai.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;p&gt;I was born in the back half of 1980, which means I missed the revolution.&lt;/p&gt;
&lt;p&gt;By the time I sat down at an &lt;a href="https://baud.rs/0ltUMy"&gt;Apple IIe&lt;/a&gt; in 1986, with its green phosphor glow and chunky 5.25-inch floppies, the war was already over. The Altair 8800 was a museum piece. CP/M was fading into obscurity. The TRS-80 and Commodore PET were yesterday's news. I arrived just in time for the Apple II's twilight years, blissfully unaware that the machine in front of me represented the victory lap of a decade-long transformation.&lt;/p&gt;
&lt;p&gt;I never experienced CP/M. I never loaded WordStar from an 8-inch floppy. I never watched VisiCalc recalculate a spreadsheet and felt the shock of a machine doing in seconds what had taken hours by hand. These were &lt;a href="https://baud.rs/dCQV1V"&gt;foundational moments in computing history&lt;/a&gt;, and I missed them entirely.&lt;/p&gt;
&lt;p&gt;Now, decades later, I find myself building &lt;a href="https://baud.rs/Ch4htI"&gt;Z80 emulators&lt;/a&gt; and writing compilers for a processor that had already ceded the PC spotlight by the time I could read, though it quietly lived on in the TI graphing calculators that would later get me through high school math, and still powers them today. It's a form of technological archaeology, reconstructing a world I never lived in, trying to understand the texture of an era I only know through documentation and nostalgia. And from this vantage point, watching the current panic over artificial intelligence, I can't help but notice something: we've been here before.&lt;/p&gt;
&lt;p&gt;As 2025 quickly fades. ChatGPT writes code. Midjourney creates art. Claude analyzes documents. The headlines scream that knowledge workers are doomed, that white-collar jobs will evaporate, that "this time it's different."&lt;/p&gt;
&lt;p&gt;But it's not different. It's never different. And VisiCalc can prove it.&lt;/p&gt;
&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/what-visicalc-teaches-us-about-ai_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;31 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;h3&gt;The World Before the Spreadsheet&lt;/h3&gt;
&lt;p&gt;To understand why VisiCalc mattered, you need to understand what "spreadsheet" meant in 1978. It wasn't software. It was paper.&lt;/p&gt;
&lt;p&gt;Accountants, analysts, and financial planners worked with literal sheets of paper, ruled into rows and columns. They called them spreadsheets because the worksheets spread across multiple pages, sometimes taped together into unwieldy grids that covered entire desks. Every number was written by hand. Every calculation was performed with a mechanical adding machine or, if you were modern, an electronic calculator.&lt;/p&gt;
&lt;p&gt;Here's what financial planning looked like: You'd spend hours, maybe days, building a projection. Revenue assumptions in one column, cost structures in another, profit margins calculated cell by cell. Then your boss would ask a simple question: "What if we increase prices by 5%?"&lt;/p&gt;
&lt;p&gt;And you'd start over.&lt;/p&gt;
&lt;p&gt;Not from the pricing cell. From every cell that pricing touched. The cascade of recalculations could take hours. A complex model might require a full day to revise. And if you made an error somewhere in the middle? Good luck finding it in a forest of pencil marks and eraser smudges.&lt;/p&gt;
&lt;p&gt;Word processing was no better. Before WordStar and its competitors, documents were produced on typewriters. The &lt;a href="https://baud.rs/jBSJa5"&gt;IBM Selectric&lt;/a&gt; was the gold standard, a marvel of engineering that let you swap font balls and correct single characters with lift-off tape. But if you found a typo on page 47 of a 60-page contract, you had options: live with it, or retype pages 47 through 60.&lt;/p&gt;
&lt;p&gt;Typing was a specialized profession. Companies maintained typing pools, rooms full of secretaries whose primary job was converting handwritten drafts and dictation into finished documents. A skilled typist was a valuable employee precisely because the work was so labor-intensive.&lt;/p&gt;
&lt;p&gt;And if you needed computing power for serious analysis, you went to the mainframe. You submitted your job to the MIS department, waited in a queue, and paid by the CPU-minute. Time-sharing systems charged hundreds of dollars per hour. Computing was a scarce resource, rationed by bureaucracy.&lt;/p&gt;
&lt;p&gt;This was knowledge work in the mid-1970s: manual, slow, expensive, and error-prone.&lt;/p&gt;
&lt;h3&gt;The Revolution No One Expected&lt;/h3&gt;
&lt;p&gt;Dan Bricklin was a Harvard MBA student in 1978 when he had the insight that would change everything. Sitting in a classroom, he watched a professor work through a financial model on a blackboard. The professor would write numbers, perform calculations, and fill in cells. Then he'd change an assumption, and the recalculation cascade would begin: erasing, recomputing, rewriting, sometimes running out of blackboard space.&lt;/p&gt;
&lt;p&gt;Bricklin's thought was simple: what if the blackboard could recalculate itself?&lt;/p&gt;
&lt;p&gt;Working with programmer Bob Frankston, Bricklin built VisiCalc, the "visible calculator." It ran on the Apple II, which was itself a hobbyist curiosity, a machine that enthusiasts bought to tinker with BASIC programs and play primitive games. VisiCalc transformed it into a business tool.&lt;/p&gt;
&lt;p&gt;The software shipped in 1979, priced at \$100. Within a year, it was selling 12,000 copies per month. More importantly, it was selling Apple IIs. The \$2,000 computer became justifiable as a business expense because VisiCalc made it productive.&lt;/p&gt;
&lt;p&gt;Consider the economics. A financial analyst in 1980 earned perhaps \$25,000 per year. A secretary earned \$12,000 to \$15,000. The Apple II plus VisiCalc cost roughly \$2,500. If the software saved a few weeks of analyst time, or let one analyst do the work that had previously required two, it paid for itself almost immediately.&lt;/p&gt;
&lt;p&gt;But the real magic wasn't cost savings; it was capability. Suddenly you could ask "what if?" as many times as you wanted. Change an assumption, watch the spreadsheet ripple with recalculations, and see the answer in seconds. Financial modeling went from a laborious exercise in arithmetic to an exploratory conversation with your data.&lt;/p&gt;
&lt;p&gt;WordStar, released a year earlier in 1978, performed the same transformation for documents. Write, edit, revise, move paragraphs, fix typos, all before committing anything to paper. The document existed as a malleable thing, not a fixed artifact produced through irreversible mechanical action.&lt;/p&gt;
&lt;p&gt;Together, these applications (and others like dBASE for databases and SuperCalc as a VisiCalc competitor) created the productivity software category. They didn't sell computers to hobbyists; they sold computers to businesses. And they did it by solving mundane problems: arithmetic and typing.&lt;/p&gt;
&lt;p&gt;The pundits of the era made predictions. Accountants would become obsolete. Secretaries would be eliminated. The typing pool would vanish. Knowledge work itself was being automated.&lt;/p&gt;
&lt;h3&gt;What Actually Happened&lt;/h3&gt;
&lt;p&gt;The predictions were wrong. Or rather, they were right about the transformation but wrong about the outcome.&lt;/p&gt;
&lt;p&gt;Typing pools did shrink. The specialized profession of "typist" largely disappeared as word processing became a universal skill. But administrative assistants didn't vanish; their job changed. Instead of spending hours producing documents, they spent hours managing calendars, coordinating logistics, and handling communication. The mechanical work evaporated; the judgment work remained.&lt;/p&gt;
&lt;p&gt;Bookkeepers declined as a profession. The person whose job was to maintain ledgers and perform routine calculations found that job automated. But accountants, the people who interpreted the numbers, made recommendations, and exercised judgment, grew in number. The Bureau of Labor Statistics shows steady growth in accounting employment through the 1980s and 1990s, even as the basic arithmetic of accounting was completely automated.&lt;/p&gt;
&lt;p&gt;Financial analysts became more valuable, not less. The spreadsheet didn't replace them; it amplified them. An analyst who could build sophisticated models in VisiCalc or Lotus 1-2-3 was worth more than one limited to paper. The ceiling rose.&lt;/p&gt;
&lt;p&gt;And here's the crucial point: the total amount of analysis, documentation, and financial modeling exploded. When something becomes cheaper and faster to produce, you produce more of it. Companies that had operated with crude annual budgets started building detailed monthly projections. Reports that had been quarterly became weekly. The volume of knowledge work grew to fill the new capacity.&lt;/p&gt;
&lt;p&gt;This pattern (automation making workers more productive, which increases demand for the work, which maintains or increases employment) has a name in economics. It's called the Jevons paradox, originally observed in coal consumption: as steam engines became more efficient, total coal usage increased rather than decreased, because efficiency made steam power economical for more applications.&lt;/p&gt;
&lt;p&gt;The same paradox applies to labor. Make an accountant 10x more productive, and you don't need 1/10th as many accountants. You do 10x as much accounting.&lt;/p&gt;
&lt;h3&gt;The Pattern Repeats&lt;/h3&gt;
&lt;p&gt;VisiCalc wasn't the first technology to trigger predictions of labor displacement, and it certainly wasn't the last. The pattern repeats with remarkable consistency:&lt;/p&gt;
&lt;p&gt;ATMs (1970s-present): Automated Teller Machines were supposed to eliminate bank tellers. The math seemed obvious: why pay a human to dispense cash when a machine could do it? Yet U.S. bank teller employment roughly doubled between 1970 and 2010. The explanation: ATMs made bank branches cheaper to operate, so banks opened more branches, each requiring fewer but still some tellers. And the tellers' jobs shifted from cash handling to sales, complex transactions, and customer relationships.&lt;/p&gt;
&lt;p&gt;CAD Software (1980s): Computer-aided design was going to eliminate draftsmen. Instead, it eliminated hand drafting while increasing demand for designers. The ability to iterate quickly, produce more alternatives, and handle more complex designs meant more design work overall.&lt;/p&gt;
&lt;p&gt;Desktop Publishing (1980s): PageMaker and QuarkXPress would kill graphic designers by letting anyone create professional documents. Instead, the volume of designed materials exploded, and graphic design became a larger profession. The average quality rose because the floor rose.&lt;/p&gt;
&lt;p&gt;Legal Research Databases (1990s): LexisNexis and Westlaw would eliminate paralegals by automating case research. Instead, faster research enabled more litigation, more thorough preparation, and more legal work overall.&lt;/p&gt;
&lt;p&gt;Electronic Trading (1990s-2000s): Algorithmic trading would eliminate floor traders and financial professionals. It did eliminate floor traders, but the financial sector's employment grew as new roles emerged: quants, algorithm developers, risk managers, compliance officers.&lt;/p&gt;
&lt;p&gt;In every case, the predictions followed the same logic: Technology X automates task Y, therefore workers who do Y are obsolete. And in every case, the predictions missed the second-order effects: automation makes the overall activity more valuable, demand increases, and workers shift to higher-judgment versions of the same work.&lt;/p&gt;
&lt;h3&gt;The AI Moment&lt;/h3&gt;
&lt;p&gt;Which brings us to now.&lt;/p&gt;
&lt;p&gt;ChatGPT was released in November 2022. Within two months, it had 100 million users. Within a year, AI assistants were embedded in products from Microsoft to Google to Adobe. &lt;a href="https://baud.rs/vtABFf"&gt;Large language models&lt;/a&gt; could write essays, generate code, summarize documents, answer questions, and produce content that was, on first glance, indistinguishable from human output.&lt;/p&gt;
&lt;p&gt;The predictions arrived immediately. Programmers would become obsolete. Writers were doomed. Customer service, legal research, financial analysis, medical diagnosis: all would be automated. Goldman Sachs estimated 300 million jobs would be affected. The World Economic Forum issued reports. Thought leaders proclaimed that "this time it's different."&lt;/p&gt;
&lt;p&gt;But is it?&lt;/p&gt;
&lt;p&gt;Let's apply the VisiCalc framework. What exactly does AI automate?&lt;/p&gt;
&lt;p&gt;First drafts, not final judgment. AI can produce a draft document, a code snippet, an analysis outline. What it cannot do is determine whether that draft serves the actual goal, handles the edge cases that matter, or fits the political context of the organization. The human reviews, revises, and takes responsibility.&lt;/p&gt;
&lt;p&gt;Pattern matching, not pattern breaking. Large language models are, at their core, sophisticated pattern matchers trained on existing text. They excel at producing outputs that look like their training data. They struggle with genuine novelty: situations unlike anything in the training corpus, problems that require inventing new approaches rather than recombining old ones.&lt;/p&gt;
&lt;p&gt;The middle of the distribution, not the edges. AI handles routine cases well. It struggles with outliers. The customer service bot can resolve common issues; the unusual complaint needs a human. The coding assistant can generate boilerplate; the architectural decision requires judgment.&lt;/p&gt;
&lt;p&gt;Production, not accountability. AI can produce outputs, but it cannot be held accountable for them. When the document goes to the client, someone signs it. When the code ships to production, someone owns it. When the decision has consequences, someone faces them. That someone is human, because accountability requires agency, and agency requires humanity.&lt;/p&gt;
&lt;p&gt;This is exactly the pattern we saw with spreadsheets. VisiCalc automated arithmetic, not judgment. It automated production, not accountability. It handled the routine middle, not the novel edges. And the humans who learned to use it became more valuable, not less.&lt;/p&gt;
&lt;h3&gt;The Irreducible Human&lt;/h3&gt;
&lt;p&gt;Why do humans remain in the loop? Not for sentimental reasons. Not because we want to preserve jobs. But because certain functions cannot be automated, regardless of how sophisticated the technology.&lt;/p&gt;
&lt;p&gt;Accountability requires agency. When something goes wrong, someone must be responsible. Legal systems, regulatory frameworks, and social structures all assume a responsible party. AI systems can produce outputs, but they cannot be sued, fired, jailed, or shamed. The human who relies on AI output remains accountable for that output. This isn't a bug; it's a feature of how human society functions.&lt;/p&gt;
&lt;p&gt;Context is infinite and local. AI models are trained on general patterns. Your specific situation (your company's politics, your client's unspoken concerns, your industry's unwritten rules) is not in the training data. The model knows what words typically follow other words. It doesn't know that your CFO hates bullet points, that your customer is going through a divorce, or that mentioning the competitor's product is forbidden in this meeting. The human provides context.&lt;/p&gt;
&lt;p&gt;Trust requires relationship. Business transactions ultimately rest on trust between humans. You hire the lawyer, not the legal database. You trust your doctor, not the diagnostic algorithm. You buy from salespeople, not recommendation engines. AI can support these relationships, but it cannot replace them, because trust is a human phenomenon.&lt;/p&gt;
&lt;p&gt;The feedback loop requires humans. Here's a subtle but critical point: AI systems are trained on human-generated data. If humans stop producing original work, the training data stops improving. The model learns to produce outputs that look like human outputs because it was trained on human outputs. Remove the humans, and you get a system trained on its own outputs, a recursive degradation. We are the curriculum.&lt;/p&gt;
&lt;p&gt;Novel situations require genuine understanding. AI excels at interpolation, finding patterns within the space of its training data. It struggles with extrapolation, handling situations outside that space. Genuine novelty, by definition, lies outside the training distribution. The unprecedented situation, the black swan event, the "we've never seen this before" moment: these require human judgment, because no pattern matching can help when there's no pattern to match.&lt;/p&gt;
&lt;h3&gt;The Reskilling Reality&lt;/h3&gt;
&lt;p&gt;None of this means AI changes nothing. It changes a lot. The question is what kind of change.&lt;/p&gt;
&lt;p&gt;When spreadsheets arrived, certain skills became less valuable. Manual arithmetic, once essential for financial work, became irrelevant. The ability to maintain error-free ledgers through careful penmanship mattered less. Slide rule proficiency joined buggy whip maintenance in the museum of obsolete competencies.&lt;/p&gt;
&lt;p&gt;But new skills became essential. Building spreadsheet models, understanding the logic of cell references, knowing how to structure data for analysis: these became core professional competencies. "Computer literacy" emerged as a job requirement. People who learned the new tools thrived; people who refused to adapt struggled.&lt;/p&gt;
&lt;p&gt;AI is triggering the same shift. Consider what becomes less valuable:&lt;/p&gt;
&lt;p&gt;Writing first drafts from scratch. When AI can produce a competent first draft in seconds, the ability to stare at a blank page and produce prose is less differentiating. The value shifts to editing, directing, and refining.&lt;/p&gt;
&lt;p&gt;Routine research and compilation. When AI can summarize documents, extract key points, and synthesize information, the human who only does that work has a problem. The value shifts to evaluating sources, asking the right questions, and interpreting results.&lt;/p&gt;
&lt;p&gt;Basic code production. When AI can generate boilerplate, implement standard patterns, and translate requirements into code, the programmer whose main skill is typing syntax is in trouble. The value shifts to architecture, debugging, code review, and understanding what the system should do.&lt;/p&gt;
&lt;p&gt;And consider what becomes more valuable:&lt;/p&gt;
&lt;p&gt;Judgment and curation. AI produces. Humans evaluate. The ability to look at AI output and quickly determine what's useful, what's wrong, and what's missing becomes essential. This is editing in the broadest sense, not just fixing typos, but directing the creative process.&lt;/p&gt;
&lt;p&gt;Domain expertise plus AI fluency. The accountant who understands both accounting and how to leverage AI tools is more valuable than either an accountant who ignores AI or an AI operator who doesn't understand accounting. The combination is the new competency.&lt;/p&gt;
&lt;p&gt;Handling exceptions and edge cases. As AI handles the routine middle, humans focus on the exceptions. The unusual customer complaint, the novel legal situation, the unprecedented technical problem: these become the human domain. Expertise in handling weirdness becomes more valuable.&lt;/p&gt;
&lt;p&gt;Relationship and trust building. As transactional work becomes automated, relationship work becomes relatively more important. The human who can build trust, navigate politics, and close deals face-to-face has a durable advantage.&lt;/p&gt;
&lt;p&gt;This is exactly what happened with spreadsheets. The value shifted from arithmetic to analysis, from production to judgment, from routine to exception. The workers who adapted thrived. The workers who clung to obsolete methods struggled.&lt;/p&gt;
&lt;h3&gt;The Transition Is Never Painless&lt;/h3&gt;
&lt;p&gt;I don't want to minimize the disruption. Real people, with real skills, face real challenges when technology shifts beneath them.&lt;/p&gt;
&lt;p&gt;The typing pool secretary in 1985 had spent years developing speed and accuracy on the Selectric. She could type 80 words per minute with minimal errors. She knew the quirks of carbon paper, the rhythm of the carriage return, the muscle memory of the key layout. These skills, honed over a decade, became worthless in the span of a few years.&lt;/p&gt;
&lt;p&gt;Some of those secretaries learned WordPerfect and became administrative assistants. Some moved into other roles entirely. Some struggled, unable or unwilling to adapt, and found themselves squeezed out of the workforce. The aggregate statistics (employment levels, productivity growth, economic expansion) hide individual stories of dislocation and difficulty.&lt;/p&gt;
&lt;p&gt;The same will be true of AI. Some knowledge workers will adapt smoothly, integrating AI tools into their workflow and becoming more productive. Some will resist, clinging to methods that worked in 2020 but feel increasingly obsolete by 2030. Some will find themselves displaced, their particular bundle of skills suddenly less valuable in a market that's moved on.&lt;/p&gt;
&lt;p&gt;The historical pattern tells us that the net outcome is positive, that technological transitions create more opportunity than they destroy, that the economy adjusts, that new roles emerge. But history is cold comfort to the individual caught in the transition. The typewriter repairman didn't care that computer technicians were a growing field. He cared that his skills were worthless.&lt;/p&gt;
&lt;p&gt;This is why the reskilling conversation matters. Not because AI will eliminate all jobs (it won't) but because the specific jobs, the specific skills, the specific ways of working will change. And navigating that change requires awareness, adaptability, and often institutional support.&lt;/p&gt;
&lt;p&gt;The workers who thrived through the spreadsheet revolution weren't necessarily the most skilled at the old methods. They were the ones who recognized the shift and moved with it. The accountant who embraced Lotus 1-2-3, even if she was mediocre at mental arithmetic, outcompeted the brilliant human calculator who refused to touch a keyboard.&lt;/p&gt;
&lt;p&gt;The same pattern is emerging now. The programmer who integrates AI assistance, even if she's not the fastest typist, will outcompete the keyboard wizard who insists on writing every character manually. The writer who uses AI for drafts and focuses on editing and judgment will outcompete the prose stylist who spends hours on first drafts. The analyst who lets AI handle data compilation and focuses on interpretation will outcompete the Excel jockey who takes pride in manual formula construction.&lt;/p&gt;
&lt;p&gt;Adaptation isn't optional. It wasn't optional in 1980, and it isn't optional now.&lt;/p&gt;
&lt;h3&gt;The Long View&lt;/h3&gt;
&lt;p&gt;I spend my weekends building emulators for 50-year-old processors. I write compilers that target the &lt;a href="https://baud.rs/n39HUo"&gt;Z80&lt;/a&gt;, a chip that was designed when Gerald Ford was president. I run BASIC and FORTH on simulated hardware, watching instructions execute that were first written when disco was young.&lt;/p&gt;
&lt;p&gt;From this perspective, the current AI moment looks familiar. Technology extends human capability. It always has. The accountant with VisiCalc wasn't replaced; she was amplified. The writer with WordStar wasn't obsolete; he was leveraged. The analyst with a spreadsheet could do in hours what had taken days, and that made analysis more valuable, not less.&lt;/p&gt;
&lt;p&gt;When I run my Z80 emulator (JavaScript interpreting WebAssembly interpreting 1976 machine code) I'm witnessing layers of abstraction that would have seemed like science fiction to the engineers who designed the original chip. But the fundamental relationship remains: humans using tools to extend their capabilities.&lt;/p&gt;
&lt;p&gt;The nature of work changes. It always changes. The bookkeeper becomes the accountant. The typist becomes the administrative assistant. The draftsman becomes the designer. The job titles shift, the tools evolve, the skills required transform. But the need for human judgment, human accountability, human creativity, and human relationships remains.&lt;/p&gt;
&lt;p&gt;This isn't optimism. It's pattern recognition. The 45-year pattern from VisiCalc to ChatGPT is consistent: technology that automates tasks changes the nature of work without eliminating the need for workers. The "this time it's different" predictions have been wrong every time, not because technology isn't powerful, but because the predictions misunderstand the relationship between automation and human labor.&lt;/p&gt;
&lt;p&gt;The spreadsheet didn't eliminate the need for human intelligence. It made human intelligence more valuable by freeing it from arithmetic. AI won't eliminate the need for human judgment. It will make human judgment more valuable by freeing it from production.&lt;/p&gt;
&lt;p&gt;We've been here before. And we'll be here again, decades from now, when some new technology triggers the same predictions, and historians look back at our AI panic the way we look back at the VisiCalc panic, as an understandable overreaction that missed the larger pattern.&lt;/p&gt;
&lt;p&gt;The work changes. The workers adapt. The need for humans persists.&lt;/p&gt;
&lt;p&gt;It's not different this time. It never is.&lt;/p&gt;</description><category>ai</category><category>automation</category><category>cp/m</category><category>economics</category><category>history</category><category>knowledge work</category><category>technology</category><category>visicalc</category><guid>https://tinycomputers.io/posts/what-visicalc-teaches-us-about-ai.html</guid><pubDate>Fri, 26 Dec 2025 18:00:00 GMT</pubDate></item><item><title>Z80 CP/M: History and Legacy and Emulation</title><link>https://tinycomputers.io/posts/cpm-history-and-legacy.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-history-and-legacy_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;44 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;&lt;img src="https://tinycomputers.io/images/gary_k_computer-chronicles.png" style="width: 640px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: left; padding: 20px 20px 20px 20px;"&gt;After reading &lt;a href="https://baud.rs/8BUo6R"&gt;an article that Adafruit&lt;/a&gt; put out on running CP/M on an emulator running on an Arduino, I thought I could expand up the article and add to the story.  Enjoy.&lt;/p&gt;
&lt;p&gt;In the early years of microcomputers, their processing power was incredibly limited compared to what we are accustomed to today. These devices, which emerged in the 1970s, were designed to be affordable and accessible for individual users, businesses, and small organizations, marking a stark departure from the large and expensive mainframes and minicomputers of the time. However, this accessibility came at a cost: the processing capabilities of these early microcomputers were constrained by the technology of the era, as well as by economic and practical considerations.&lt;/p&gt;
&lt;p&gt;One of the initial limitations of early microcomputers was the processor itself. Early models, such as the Altair 8800 and Apple I, relied on 8-bit microprocessors like the Intel 8080 and MOS 6502. These 8-bit processors could typically handle only very simple calculations and operations in comparison to more advanced processors. Clock speeds were also significantly lower; they generally ranged from under 1 MHz to a few MHz. This lack of processing speed constrained the tasks that these computers could perform; complex calculations, large datasets, and intricate simulations were largely beyond their reach.&lt;/p&gt;
&lt;p&gt;Memory was another significant limiting factor. Early microcomputers were equipped with a very small amount of RAM, often measured in kilobytes rather than the gigabytes or terabytes commonplace today. The limited RAM constrained the size and complexity of the programs that could be run, as well as the amount of data that could be processed at one time. It was not uncommon for users to constantly manage their memory use meticulously, choosing which programs and data could be loaded into their precious few kilobytes of RAM. &lt;/p&gt;
&lt;p&gt;Storage capacity in early microcomputers was also quite constrained. Hard drives were expensive and uncommon in the earliest microcomputers, which often used cassette tapes or floppy disks for data storage. These mediums offered extremely limited storage capacity, often on the order of a few tens or hundreds of kilobytes. This required users to be extremely judicious with how they used and stored data and software, as the total available storage space was minuscule compared to today's standards.&lt;/p&gt;
&lt;p&gt;In addition to hardware limitations, the software available for early microcomputers was often rudimentary due to the limited processing power. Graphical interfaces were virtually non-existent in the earliest microcomputers, with users typically interacting with the system through text-based command-line interfaces. Software applications were often basic and focused on simple tasks, such as word processing or basic spreadsheet calculations. Sophisticated applications like advanced graphics editing, video processing, or 3D modeling were well beyond the capabilities of these early systems.&lt;/p&gt;
&lt;p&gt;Against this burgeoning backdrop of the microcomputer revolution, a man by the name of &lt;a href="https://baud.rs/MJlMBx"&gt;Gary Kildall&lt;/a&gt; developed the &lt;a href="https://baud.rs/3YiduS"&gt;Control Program for Microcomputers&lt;/a&gt; (CP/M) system. CP/M was a pre-MS-DOS operating system.  Kildall, while working at Intel, developed a high-level language named &lt;a href="https://baud.rs/KZacqy"&gt;PL/M&lt;/a&gt; (Programming Language for Microcomputers). He needed a way to test and debug programs written in PL/M on the newly developed Intel 8080 microprocessor. This led to the creation of CP/M. Recognizing the imminent proliferation of different hardware systems, Kildall, with his experience at Intel and knowledge of microprocessors, saw a need for a standardized software platform. Many microcomputers were operating on incompatible systems, and Kildall's solution was CP/M, an operating system designed to work across diverse hardware setups.&lt;/p&gt;
&lt;p&gt;At the heart of CP/M's design was its modularity, characterized predominantly by the BIOS (Basic Input/Output System). The BIOS acted as an intermediary layer that handled the direct communication with the hardware, such as disk drives, keyboards, and displays. By isolating system-specific hardware instructions within the BIOS, CP/M maintained a core set of generic commands. This modular architecture meant that to make CP/M compatible with a new machine, only the BIOS needed to be tailored to the specific hardware, preserving the integrity of the rest of the operating system. This modularity enabled rapid porting of CP/M across a wide array of early microcomputers without rewriting the entire OS.&lt;/p&gt;
&lt;p&gt;Another notable technical feature of CP/M was its file system. CP/M used a disk-oriented file system, which was one of the first to use a hierarchical directory structure. This structure allowed users to organize and manage files efficiently on floppy disks. The operating system employed a simple &lt;em&gt;8.3&lt;/em&gt; filename convention (up to 8 characters for the filename and 3 for the extension) which, though limited by today's standards, was effective for the time. Files were accessed through File Control Blocks (FCBs), a data structure that provided a consistent interface for file operations, further simplifying application development.&lt;/p&gt;
&lt;p&gt;CP/M's command-line interface (CLI) was a hallmark feature, providing users with a means to interact with the system and run applications. The CLI, while rudimentary by today's standards, allowed users to navigate the directory structure, execute programs, and manage files. Coupled with a set of basic utilities bundled with the OS, this interface provided an accessible environment for both end-users and developers. For developers, CP/M provided a BDOS (Basic Disk Operating System) interface, allowing applications to be written without deep knowledge of the underlying hardware, thus fostering a rich ecosystem of software tailored for the CP/M platform.&lt;/p&gt;
&lt;p&gt;However, CP/M's technical success didn't guarantee lasting market dominance. As it gained traction, Kildall's company, Digital Research, became a major player in the microcomputer software industry. But a missed business opportunity with IBM led to IBM choosing Microsoft's MS-DOS, which bore similarities to CP/M, for its Personal Computer.  The story of early personal computing is interesting, and is depicted nicely in &lt;a href="https://baud.rs/SS1LRV"&gt;Pirates of Silicon Valley&lt;/a&gt; (available on &lt;a href="https://baud.rs/yUSal3"&gt;DVD&lt;/a&gt;). The IBM + MS-DOS choice tilted the scales in the software market, positioning MS-DOS and its successors as major players, while CP/M gradually faded. Nonetheless, CP/M's role in early personal computing is significant, representing a key step towards standardized operating systems.&lt;/p&gt;
&lt;p&gt;I wasn't around for the early days of personal computing when CP/M was a big deal. By the time I started exploring computers in the mid-1980s, the Apple IIe was the choice in education where I was first really exposed to personal computers.  The Apple IIe was straightforward and easy to use. When I turned it on, I was met with the AppleSoft BASIC interface. In 1992, as I would soon be a teenager, my family purchased its first personal computer from &lt;a href="https://baud.rs/h9i9X9"&gt;Gateway 2000&lt;/a&gt;. Even though I missed the CP/M phase, the Apple IIe provided a solid introduction to the world of computing for me with the Gateway 2000 being foundational in my ever growing interest in computers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Let's get back to CP/M.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The primary architecture CP/M was designed for was the Intel 8080 and its compatible successor, the Zilog Z80. However, CP/M was adapted to run on several different architectures over time. Here's a brief overview of some architectures and their technical specs:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://baud.rs/qj3fl7"&gt;&lt;strong&gt;Intel 8080&lt;/strong&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;8-bit microprocessor&lt;/li&gt;
&lt;li&gt;Clock speeds typically up to 2 MHz&lt;/li&gt;
&lt;li&gt;4.5k transistors&lt;/li&gt;
&lt;li&gt;16-bit address bus, enabling it to access 65,536 memory locations&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://baud.rs/IboIHD"&gt;&lt;strong&gt;Zilog Z80&lt;/strong&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;8-bit microprocessor&lt;/li&gt;
&lt;li&gt;Clock speeds of 2.5 MHz to 10 MHz&lt;/li&gt;
&lt;li&gt;Around 8.5k transistors&lt;/li&gt;
&lt;li&gt;16-bit address bus, 8-bit data bus&lt;/li&gt;
&lt;li&gt;It had enhanced instruction set compared to 8080 and was binary compatible with it.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://baud.rs/Y96u3D"&gt;&lt;strong&gt;Intel 8085&lt;/strong&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;8-bit microprocessor&lt;/li&gt;
&lt;li&gt;Clock speeds of up to 5 MHz&lt;/li&gt;
&lt;li&gt;An improved and more power-efficient version of the 8080&lt;/li&gt;
&lt;li&gt;Included new instructions over the 8080&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://baud.rs/FBfdi6"&gt;&lt;strong&gt;Zilog Z8000&lt;/strong&gt;&lt;/a&gt; and &lt;strong&gt;&lt;a href="https://baud.rs/Y96u3D"&gt;Intel 8086&lt;/a&gt;/&lt;a href="https://baud.rs/FFHKs6"&gt;8088&lt;/a&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;These were 16-bit processors.&lt;/li&gt;
&lt;li&gt;CP/M-86 was developed for these processors as an extension to the original 8-bit CP/M.&lt;/li&gt;
&lt;li&gt;The 8086 had a 16-bit data bus, and the 8088, used in the original IBM PC, had an 8-bit data bus.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://baud.rs/fJY2GX"&gt;&lt;strong&gt;Motorola 68000&lt;/strong&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;While not a primary platform for CP/M, there were ports and adaptations made for the 16/32-bit Motorola 68000 series.&lt;/li&gt;
&lt;li&gt;Used in early Apple Macintosh computers, Atari ST, Commodore Amiga, and others.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://baud.rs/ngwX8G"&gt;&lt;strong&gt;Interdata 7/32&lt;/strong&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;This is a lesser-known 32-bit minicomputer for which CP/M was adapted.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We have already looked at the Z80 (in the context of the &lt;a href="https://tinycomputers.io/posts/exploring-ti-84%2B.html"&gt;TI-84+ graphing calculator&lt;/a&gt;) as well as the Motorola 68000 (in the context of the &lt;a href="https://tinycomputers.io/posts/motorola-68000-processor-and-the-ti-89-graphing-calculator.html"&gt;TI-89 graphing calculator&lt;/a&gt;).  Instead of focusing on a specific architecture, the &lt;a href="https://baud.rs/ezLbYx"&gt;RC2014&lt;/a&gt;, to run CP/M on bare metal, we will be looking at running a CP/M emulator on Adafruit's &lt;a href="https://baud.rs/CJ8gaf"&gt;Grand Central M4 Express&lt;/a&gt;.  I would love to get of the RC2014 kits and run CP/M on bare metal, but for now, we won't be doing that.&lt;/p&gt;
&lt;p&gt;We're concentrating on setting up &lt;a href="https://baud.rs/aUvSkU"&gt;RunCPM&lt;/a&gt; on the Grand Central, so we'll only touch on the Z80 briefly. For additional information on the Z80, visit &lt;a href="https://tinycomputers.io/posts/z80.info"&gt;z80.info&lt;/a&gt;. The person behind z80.info also write an in-depth look at Z80 hardware and assembly language in &lt;a href="https://baud.rs/yuJUcG"&gt;Hackspace Magazine&lt;/a&gt; issues &lt;a href="https://baud.rs/Gfr38r"&gt;7&lt;/a&gt; &amp;amp; &lt;a href="https://baud.rs/Ft8JHZ"&gt;8&lt;/a&gt;. If you're interested in a comprehensive study of the Z80, consider the books: &lt;a href="https://baud.rs/An2wC8"&gt;Build your own Z80 computer&lt;/a&gt; - design guidelines and application notes by Steve Ciarcia (you can also grab the PDF &lt;a href="https://baud.rs/OEWJGQ"&gt;here&lt;/a&gt; or &lt;a href="https://baud.rs/pwkUkE"&gt;here&lt;/a&gt;) and &lt;a href="https://baud.rs/SW8mZG"&gt;Programming the Z80&lt;/a&gt; by Rodnay Zaks (you can also grab PDFs &lt;a href="https://baud.rs/ERQQHu"&gt;here&lt;/a&gt;, or &lt;a href="https://baud.rs/5h0dRe"&gt;here&lt;/a&gt; or &lt;a href="https://baud.rs/yrUff8"&gt;here&lt;/a&gt; or &lt;a href="https://baud.rs/6LVgpA"&gt;here&lt;/a&gt;).  Both books have out of print for decades and are rather expensive on Amazon.&lt;/p&gt;
&lt;hr&gt;

&lt;h3&gt;CP/M&lt;/h3&gt;
&lt;p&gt;CP/M incorporated wildcard characters in its file naming conventions, a legacy we continue to see in modern systems. Specifically, '?' was used to match any single character, and '*' could match part of or an entire file name or file type.&lt;/p&gt;
&lt;p&gt;In terms of commands, many accepted these wildcards, and such a command was labeled as using an ambiguous file reference, abbreviated as "afn". In contrast, commands that required file references to be specific, without the use of wildcards, were termed as using an unambiguous file reference or "ufn". These shorthand terms, "afn" and "ufn", are frequently found in official CP/M documentation and will be adopted for our discussion here.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Builtin Commands:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DIR afn&lt;/strong&gt; (or simply &lt;strong&gt;DIR&lt;/strong&gt;): Employed to display the names of files that match the specified wildcard pattern.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ERA afn&lt;/strong&gt;: This command is used to delete one or multiple files.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;REN ufn1=ufn2&lt;/strong&gt;: As the name suggests, this command allows users to rename a specific file.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;TYPE ufn&lt;/strong&gt;: Useful for viewing the contents of an ASCII file..&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Standard Programs:&lt;/h3&gt;
&lt;p&gt;CP/M was equipped with a suite of standard programs, often referred to as Transient Commands. These weren't embedded within the core of CP/M but were accessible to the user as needed. They'd be loaded, run, and then purged from the memory. Several of these commands were fundamental for operations within the CP/M environment. A concise overview of some notable Transient Commands is provided below, though a more exhaustive exploration can be found in the CP/M manual.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;STAT&lt;/strong&gt;: This program offers insights into the current disk's status, specifics about individual files, and device assignment details.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ASM&lt;/strong&gt;: A tool for program assembly. It takes a source code input and assembles it to produce an executable.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;LOAD&lt;/strong&gt;: Designed for Intel HEX formatted code files, this command loads the code and subsequently converts it into an executable format.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DDT&lt;/strong&gt;: This is CP/M's built-in debugger, essential for diagnosing and resolving program errors.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ED&lt;/strong&gt;: CP/M's text editor, enabling users to create and modify text files within the operating system.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SUBMIT&lt;/strong&gt;: A utility to accept a file containing a list of commands, essentially enabling batch processing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DUMP&lt;/strong&gt;: A handy tool for those looking to view a file's contents represented in hexadecimal format.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For those eager to dive deeper into the vast ocean of CP/M's capabilities and legacy, the &lt;a href="https://baud.rs/8bleeY"&gt;Tim Olmstead Memorial Digital Research CP/M Library&lt;/a&gt; is an invaluable resource, housing a trove of information and code associated with CP/M.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/runcpm/Screenshot_20230819_142558.png" style="width: 640px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: left; padding: 20px 20px 20px 20px;"&gt;&lt;/p&gt;
&lt;p&gt;RunCPM is essentially a Z80 emulator that comes packaged with different CP/M versions tailored to function on the emulated Z80. It's a comprehensive toolkit for those interested in delving into Z80 assembly language programming, with the added perk of accessing the Grand Central's IO capabilities. As a bonus, &lt;a href="https://baud.rs/oFakmj"&gt;Microsoft Basic&lt;/a&gt; is incorporated within the package, and for enthusiasts looking to explore further, various &lt;a href="https://baud.rs/kxlbrC"&gt;other languages&lt;/a&gt; can be sourced online. One such language is Modula-2, which holds significance as Niklaus Wirth's successor to the famed &lt;a href="https://baud.rs/UazcpF"&gt;Pascal language&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;When it comes to building RunCPM, the approach isn't one-size-fits-all. The build method you opt for is contingent on the target platform. In our case, we're aiming for compatibility with the Grand Central, so the Arduino method is the route we'll take. Begin by launching the &lt;code&gt;RunCPM.ino&lt;/code&gt; file within the Arduino IDE (or Visual Code). However, prior to this step, ensure that the IDE is configured to build for the &lt;a href="https://baud.rs/2g1Bz5"&gt;Grand Central&lt;/a&gt;.  The following are stripped down &lt;a href="https://baud.rs/2SPjMg"&gt;instructions for RunCPM from its Github repo&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;RunCPM - Z80 CP/M emulator&lt;/h2&gt;
&lt;p&gt;RunCPM is an application which can execute vintage CP/M 8 bits programs on many modern platforms, like Windows, Mac OS X, Linux, FreeBSD, Arduino DUE and variants, like &lt;a href="https://baud.rs/GSOYjU"&gt;Adafruit Grand Central Station&lt;/a&gt;, and the &lt;a href="https://baud.rs/TKyTZ8"&gt;Teensy&lt;/a&gt; or &lt;a href="https://baud.rs/2FEVum"&gt;ESP32&lt;/a&gt;. It can be built both on 32 and 64 bits host environments and should be easily portable to other platforms.&lt;/p&gt;
&lt;p&gt;RunCPM is fully written in C and in a modular way, so porting to other platforms should be only a matter of writing an abstraction layer file for it. No modification to the main code modules should be necessary.&lt;/p&gt;
&lt;p&gt;If you miss using powerful programs like &lt;a href="https://baud.rs/ByyoOH"&gt;Wordstar&lt;/a&gt;, &lt;a href="https://baud.rs/nNUJCr"&gt;dBaseII&lt;/a&gt;, &lt;a href="https://baud.rs/liMex3"&gt;mBasic&lt;/a&gt; and others, then RunCPM is for you. It is very stable and fun to use.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/runcpm/IMG_1732.jpg" style="width: 640px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: right; padding: 20px 20px 20px 20px;"&gt;&lt;/p&gt;
&lt;p&gt;RunCPM emulates CP/M from &lt;a href="https://baud.rs/AU3fB3"&gt;Digital Research&lt;/a&gt; as close as possible, the only difference being that it uses regular folders on the host instead of disk images.&lt;/p&gt;
&lt;h3&gt;&lt;a href="https://baud.rs/2g1Bz5"&gt;Grand Central M4&lt;/a&gt; (GSM4)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The ATSAMD51 is large with an Arduino Mega shape and pinout.&lt;/li&gt;
&lt;li&gt;The front half has the same shape and pinout as Adafruit's Metro's, so it is compatible with all Adafruit shields.&lt;/li&gt;
&lt;li&gt;It's got analog pins, and SPI/UART/I2C hardware support in the same spot as the Metro 328 and M0.&lt;/li&gt;
&lt;li&gt;It's powered with an ATSAMD51P20, which includes:&lt;ul&gt;
&lt;li&gt;Cortex M4 core running at 120 MHz&lt;/li&gt;
&lt;li&gt;Floating point support with Cortex M4 DSP instructions&lt;/li&gt;
&lt;li&gt;1MB flash, 256 KB RAM&lt;/li&gt;
&lt;li&gt;32-bit, 3.3V logic and power&lt;/li&gt;
&lt;li&gt;70 GPIO pins in total&lt;/li&gt;
&lt;li&gt;Dual 1 MSPS DAC (A0 and A1)&lt;/li&gt;
&lt;li&gt;Dual 1 MSPS ADC (15 analog pins)&lt;/li&gt;
&lt;li&gt;8 x hardware SERCOM (can be I2C, SPI or UART)&lt;/li&gt;
&lt;li&gt;22 x PWM outputs&lt;/li&gt;
&lt;li&gt;Stereo I2S input/output with MCK pin&lt;/li&gt;
&lt;li&gt;12-bit Parallel capture controller (for camera/video in)&lt;/li&gt;
&lt;li&gt;Built-in crypto engines with AES (256 bit), true RNG, Pubkey controller&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Power the Grand Central with 6-12V polarity protected DC or the micro USB connector to any 5V USB source.&lt;/li&gt;
&lt;li&gt;The 2.1mm DC jack has an on/off switch next to it so you can turn off your setup easily.&lt;/li&gt;
&lt;li&gt;The board will automagically switch between USB and DC.&lt;/li&gt;
&lt;li&gt;Grand Central has 62 GPIO pins, 16 of which are analog in, and two of which are true analog out.&lt;/li&gt;
&lt;li&gt;There's a hardware SPI port, hardware I2C port, and hardware UART. &lt;/li&gt;
&lt;li&gt;5 more SERCOMs are available for extra I2C/SPI/UARTs.&lt;/li&gt;
&lt;li&gt;Logic level is 3.3V.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The GC M4 comes with native USB support, eliminating the need for a separate hardware USB to Serial converter. When configured to emulate a serial device, this USB interface enables any computer to send and receive data to the GC M4. Moreover, this interface can be used to launch and update code via the bootloader. The board’s USB support extends to functioning as a Human Interface Device (HID), allowing it to act like a keyboard or mouse, which can be a significant feature for various interactive projects.&lt;/p&gt;
&lt;p&gt;On the hardware front, the GC M4 features four indicator LEDs and one NeoPixel located on the front edge of the PCB, designed for easy debugging and status indication. The set includes one green power LED, two RX/TX LEDs that indicate data transmission over USB, and a red LED connected to a user-controllable pin. Adjacent to the reset button, there is an RGB NeoPixel. This NeoPixel can be programmed to serve any purpose, such as displaying a status color code, which adds a visually informative aspect to your projects.&lt;/p&gt;
&lt;p&gt;Furthermore, the GC M4 includes an 8 MB QSPI (Quad SPI) Flash storage chip on board. This storage can be likened to a miniature hard drive embedded within the microcontroller. In a CircuitPython environment, this 8 MB flash memory serves as the storage space for all your scripts, libraries, and files, effectively acting as the "disk" where your Python code lives. When the GC M4 is used in an Arduino context, this storage allows for read/write operations, much like a small data logger or an SD card. A dedicated helper program is provided to facilitate accessing these files over USB, making it easy to transfer data between the GC M4 and a computer. This built-in storage is a significant feature, as it simplifies the process of logging data and managing code, and it opens up new possibilities for more advanced and storage-intensive projects.&lt;/p&gt;
&lt;p&gt;The GC M4 board boasts a built-in Micro SD Card slot, providing a convenient and flexible option for removable storage of any size. This storage is connected to an SPI (Serial Peripheral Interface) SERCOM, providing high-speed data communication. Notably, SDIO (Secure Digital Input Output), a faster interface that is commonly used for SD cards, is not supported on this board. Nevertheless, the availability of a dedicated Micro SD Card slot is a standout feature, as it allows users to easily expand the storage capacity of their projects without any complex setup. This integrated Micro SD Card slot is a substantial advantage when comparing the GC M4 to other boards, such as the Arduino Due. Unlike the GC M4, the Arduino Due does not come with built-in SD card support. For projects that require additional storage or data logging capabilities on the Due, users must purchase and connect an external &lt;a href="https://baud.rs/avZcYD"&gt;Micro SD adapter&lt;/a&gt; or a &lt;a href="https://baud.rs/utdvRd"&gt;shield&lt;/a&gt;, which can add to the overall cost and complexity of the setup. The built-in SD Card slot on the GC M4 eliminates the need for such additional components, simplifying project designs and reducing both the cost and the physical footprint of the final build. &lt;/p&gt;
&lt;p&gt;This convenient feature underscores the GC M4's design philosophy of providing an integrated, user-friendly experience. By including an SD Card slot directly on the board, the GC M4 encourages broader experimentation with data-intensive applications, such as data logging, file storage, and multimedia processing, which can be essential for a wide range of creative and practical projects.&lt;/p&gt;
&lt;p&gt;Comes pre-loaded with the UF2 bootloader, which looks like a USB storage key. Simply drag firmware on to program, no special tools or drivers needed! It can be used to load up CircuitPython or Arduino IDE (it is &lt;a href="https://baud.rs/6ezWH3"&gt;bossa&lt;/a&gt; v1.8 compatible)&lt;/p&gt;
&lt;p&gt;With all of these features, it probably seems like cheating for getting CP/M working. And we will be barely exercising these features.  If only Gary Kildall could see how computers and technology have evolved.&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;/p&gt;
&lt;h3&gt;Grand Central Specific Adaptations for RunCMP&lt;/h3&gt;
&lt;p&gt;Arduino digital and analog read/write support was added by Krzysztof Kliś via extra non-standard BDOS calls (see the bottom of &lt;a href="https://baud.rs/vgYM1g"&gt;cpm.h&lt;/a&gt; file for details).&lt;/p&gt;
&lt;p&gt;LED blink codes: GSM4 user LED will blink fast when RunCPM is waiting for a serial connection and will send two repeating short blinks when RunCPM has exited (CPU halted). Other than that the user LED will indicate disk activity.&lt;/p&gt;
&lt;p&gt;RunCPM needs A LOT of RAM and Flash memory by Arduino standards, so you will need to run on Arduinos like the DUE (not the Duemilanove) and similar controllers, like &lt;a href="https://baud.rs/fz1L4a"&gt;Adafruit's Grand Central&lt;/a&gt;.  It is theoretically possible to run it on an Arduino which has enough Flash (at least 96K) by adding external RAM to it via some shield, but this is untested, probably slow and would require an entirely different port of RunCPM code.  That could be for another day, but if you want to get CP/M running quickly, grab a Grand Central or &lt;a href="https://baud.rs/6ghn0j"&gt;Due&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You will also need a &lt;a href="https://baud.rs/1vcd71"&gt;micro sd ("tf") card&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;When using Arduino boards, the serial speed as well as other parameters, may be set by editing the &lt;code&gt;RunCPM.ino&lt;/code&gt; sketch. The default serial speed is 9600 for compatibility with vintage terminals.&lt;/p&gt;
&lt;p&gt;You will need to clone the RunCPM repository:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://github.com/MockbaTheBorg/RunCPM.git&lt;span class="w"&gt; &lt;/span&gt;-v
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In &lt;code&gt;RunCPM.ino&lt;/code&gt;, you will want to specify the Grand Center header file be included:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;"hardware/arduino/gc.h"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;instead of&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;"hardware/arduino/due_sd_tf.h"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;hr&gt;

&lt;h3&gt;Getting Started&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Preparing the RunCPM folder :&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To set up the RunCPM environment, create a folder that contains both the RunCPM executable and the CCP (Console Command Processor) binaries for the system. Two types of CCP binaries are provided: one for 64K memory and another for 60K memory.  On your micro SD card, you will want to create a directory called &lt;code&gt;A&lt;/code&gt; which will need a directory called &lt;code&gt;0&lt;/code&gt; in it.  Place in &lt;code&gt;0&lt;/code&gt; the contents of &lt;code&gt;A.zip&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The 64K version of the CCPs maximizes the amount of memory available to CP/M applications. However, its memory addressing ranges are not reflective of what a real CP/M computer would have, making it less authentic in terms of emulating a physical CP/M machine.&lt;/p&gt;
&lt;p&gt;On the other hand, the 60K version of the CCPs aims to provide a more realistic memory addressing space. It maintains the CCP entry point at the same loading address that it would occupy on a physical CP/M computer, adding to the authenticity of the emulation.&lt;/p&gt;
&lt;p&gt;While the 64K and 60K versions are standard, it is possible to use other memory sizes, but this would necessitate rebuilding the CCP binaries. The source code needed to do this is available on disk A.ZIP. The CCP binaries are named to correspond with the amount of memory they are designed to operate with. For example, DRI's CCP designed for a 60K memory environment would be named CCP-DR.60K. RunCPM searches for the appropriate file based on the amount of memory selected when it is built.&lt;/p&gt;
&lt;p&gt;It is important to note that starting with version 3.4 of RunCPM, regardless of the amount of memory allocated to the CP/M system, RunCPM will allocate 64K of RAM on the host machine. This ensures that the BIOS always starts at the same position in memory. This design decision facilitates the porting of an even wider range of CCP codes to RunCPM. Starting from version 3.4, it is essential to use new copies of the master disk A.ZIP, as well as the ZCPR2 CCP and ZCPR3 CCP (all of which are provided in the distribution).&lt;/p&gt;
&lt;h3&gt;Building dependencies&lt;/h3&gt;
&lt;p&gt;All boards now use the SdFat 2.x library, from here: &lt;a href="https://baud.rs/ZBaXhV"&gt;https://github.com/greiman/SdFat/&lt;/a&gt;
All Arduino libraries can be found here: &lt;a href="https://baud.rs/JwjmKF"&gt;https://www.arduinolibraries.info/&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;SdFat library change&lt;/h3&gt;
&lt;p&gt;If you get a &lt;strong&gt;'File' has no member named 'dirEntry'&lt;/strong&gt; error, then a modification is needed on the SdFat Library &lt;code&gt;SdFatConfig.h&lt;/code&gt; file (line 78 as of version 2.0.2) changing:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;#define SDFAT_FILE_TYPE 3&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;to&lt;/p&gt;
&lt;p&gt;&lt;code&gt;#define SDFAT_FILE_TYPE 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;As file type 1 is required for most of the RunCPM ports.&lt;/p&gt;
&lt;p&gt;To find your libraries folder, open the Preferences in Arduino IDE and look at the Sketchbook location field. &lt;/p&gt;
&lt;h4&gt;Changes to Adapt to the Grand Central&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Given that the official repository has already integrated the modifications to support the Grand Central, the following changes are primarily to serve educational purposes or as guidance for those intending to adapt the setup for other M4 boards.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;All of the following should already be set in &lt;code&gt;RunCPM.ino&lt;/code&gt;, but I'll write them out so you can see what changes have been made.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;abstraction_arduino.h&lt;/strong&gt;
For the Grand Central, the alteration pertains to the setting of HostOs:&lt;/p&gt;
&lt;p&gt;On line 8, the line:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#ifdef ARDUINO_SAM_DUE&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Should be transformed to:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#if defined ARDUINO_SAM_DUE || defined ADAFRUIT_GRAND_CENTRAL_M4&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;RunCPM.ino&lt;/strong&gt;
Aligning with the alteration in &lt;code&gt;abstraction_arduino.h&lt;/code&gt;, we also need to integrate Grand Central support in this file. Specifically, configurations relating to the SD card, LED interfaces, and the board's designation need adjustment. Insert a branch to the board configuration &lt;code&gt;#if&lt;/code&gt; structure at approximately line 28:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#elif defined ADAFRUIT_GRAND_CENTRAL_M4&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="cp"&gt;#define USE_SDIO 0&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;SdFat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SD&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="cp"&gt;#define SDINIT SDCARD_SS_PIN&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="cp"&gt;#define LED 13&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="cp"&gt;#define LEDinv 0&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="cp"&gt;#define BOARD "ADAFRUIT GRAND CENTRAL M4"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Due to certain ambiguous factors (perhaps the unique SPI bus configuration for the SD card), initializing the SD card and file system requires a different approach. Thus, following the insertion of the previous snippet, at line 108:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#if defined ADAFRUIT_GRAND_CENTRAL_M4&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cardBegin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SDINIT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SD_SCK_MHZ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&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;SD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fsBegin&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;_puts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;File System initialization failed.&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="w"&gt;      &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="cp"&gt;#else&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SDINIT&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="cp"&gt;#endif&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This snippet replaces the original:&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;SD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SDINIT&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;Following these modifications, it's straightforward to get RunCPM functional. For communication, the USB connection acts as the terminal interface. However, take note that not all terminal emulators provide flawless compatibility. Since CP/M anticipates a VT100-style terminal, some features might not behave as expected.&lt;/p&gt;
&lt;h3&gt;Installing Adafruit SAMD M4 Boards&lt;/h3&gt;
&lt;p&gt;If you haven't already, you will need to add Adafruit board definitions to Arduino IDE.  To do this, copy the URL below and paste into the text field in the dialog box; navigate to &lt;code&gt;File --&amp;gt; Preferences&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://baud.rs/zYryWC"&gt;https://adafruit.github.io/arduino-board-index/package_adafruit_index.json&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We will only need to add one URL to the IDE in this example, but you can add multiple URLS by separating them with commas. Copy and paste the link below into the Additional Boards Manager URLs option in the Arduino IDE preferences.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/runcpm/Screenshot_20230818_203425.png" style="width: 640px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: left; padding: 20px 20px 20px 20px;"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/runcpm/Screenshot_20230818_202355.png" style="width: 640px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: left; padding: 20px 20px 20px 20px;"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Preparing the CP/M virtual drives :&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;VERY IMPORTANT NOTE&lt;/strong&gt; - Starting with RunCPM version 3.7, the use of user areas has become mandatory. The support for disk folders without user areas was dropped between versions 3.5 and 3.6. If you are running a version up to 3.5, it is advisable to consider upgrading to version 3.7 or higher. However, before making this move, it is important to update your disk folder structure to accommodate the newly required support for user areas.&lt;/p&gt;
&lt;p&gt;RunCPM emulates the disk drives and user areas of the CP/M operating system by means of subfolders located under the RunCPM executable’s directory. To prepare a folder or SD card for running RunCPM, follow these procedures:&lt;/p&gt;
&lt;p&gt;Create subfolders in the location where the RunCPM executable is located. Name these subfolders "A", "B", "C", and so forth, corresponding to each disk drive you intend to use. Each one of these folders represents a separate disk drive in the emulated CP/M environment. Within the "A" folder, create a subfolder named "0". This represents user area 0 of disk A:. Extract the contents of the A.ZIP package into this "0" subfolder. When you switch to another user area within CP/M, RunCPM will automatically create the respective subfolders, named "1", "2", "3", etc., as they are selected. For user areas 10 through 15, subfolders are created with names "A" through "F".&lt;/p&gt;
&lt;p&gt;It is crucial to keep all folder and file names in uppercase to avoid potential issues with case-sensitive filesystems. CP/M originally supported only 16 disk drives, labeled A: through P:. Therefore, creating folder names representing drives beyond P: will not function in the emulation, and the same limitation applies to user areas beyond 15 (F).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Available CCPs :&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;RunCPM can run on its internal CCP or using binary CCPs from real CP/M computers. A few CCPs are provided:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CCP-DR&lt;/strong&gt; - Is the original CCP from Digital Research.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CCP-CCPZ&lt;/strong&gt; - Is the Z80 CCP from RLC and others.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CCP-ZCP2&lt;/strong&gt; - Is the original ZCPR2 CCP modification.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CCP-ZCP3&lt;/strong&gt; - Is the original ZCPR3 CCP modification.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CCP-Z80&lt;/strong&gt; - Is the Z80CCP CCP modification, also from RLC and others.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The A.ZIP package includes the source code for the Console Command Processors (CCPs), allowing for native rebuilding if necessary. To facilitate this, SUBMIT (.SUB) files are provided, which are also useful for rebuilding some of the RunCPM utilities. &lt;/p&gt;
&lt;p&gt;While the package comes with a set of CCPs, users can adapt additional CCPs to work with RunCPM. If successful in this adaptation, users are encouraged to share their work so it can be potentially added to the package for others to use. By default, RunCPM utilizes an internal CCP. However, if you prefer to use a different CCP, two specific steps must be taken, which are outlined below:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1 -&lt;/strong&gt; Change the selected CCP in globals.h (in the RunCPM folder). Find the lines that show:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;/&lt;em&gt; Definition of which CCP to use (must define only one) &lt;/em&gt;/&lt;/p&gt;
&lt;p&gt;#define CCP_INTERNAL // If this is defined, an internal CCP will emulated&lt;/p&gt;
&lt;p&gt;//#define CCP_DR&lt;/p&gt;
&lt;p&gt;//#define CCP_CCPZ&lt;/p&gt;
&lt;p&gt;//#define CCP_ZCPR2&lt;/p&gt;
&lt;p&gt;//#define CCP_ZCPR3&lt;/p&gt;
&lt;p&gt;//#define CCP_Z80&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Comment out the CCP_INTERNAL line by inserting two slashes at the line's beginning. Then remove the two slashes at the start of the line containing the name of the CCP you intend to use. Save the file.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2 -&lt;/strong&gt; Copy a matching CCP from the CCP folder to the folder that holds your A folder. Each CCP selection will have two external CCP's, one for 60K and another for 64K. If you have already built the executable, you will need to do it again.&lt;/p&gt;
&lt;p&gt;Anytime you wish to change the CCP, you must repeat these steps and rebuild.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;IMPORTANT NOTE&lt;/strong&gt; - CCP-Z80 expects the $$$.SUB to be created on the currently logged drive/user, so when using it, use SUBMITD.COM instead of SUBMIT.COM when starting SUBMIT jobs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Contents of the "master" disk (A.ZIP) :&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The "master" disk, labeled as A.ZIP, serves as the foundational environment for CP/M within RunCPM. It includes the source code for the Console Command Processors (CCPs) and features the EXIT program, which terminates RunCPM when executed. &lt;/p&gt;
&lt;p&gt;The master disk also houses the FORMAT program, designed to create a new drive folder, simulating the process of formatting a disk. Importantly, the FORMAT program doesn't affect existing drive folders, ensuring its safe use. Despite its ability to create these drive folders, it doesn't have the capability to remove them from within RunCPM. To remove a drive folder created by the FORMAT program, manual deletion is necessary, which involves accessing the RunCPM folder or SD Card via a host machine. &lt;/p&gt;
&lt;p&gt;In addition to these utilities, the master disk contains Z80ASM, a potent Z80 assembler that directly produces .COM files, ready for execution. To further enhance the RunCPM experience, the master disk also includes various CP/M applications not originally part of Digital Research Inc.'s (DRI's) official distribution. A detailed list of these additional applications can be found in the 1STREAD.ME file included on the master disk.&lt;/p&gt;
&lt;h3&gt;Printing&lt;/h3&gt;
&lt;p&gt;Printing to the PUN: and LST: devices is allowed and will generate files called "PUN.TXT" and "LST.TXT" under user area 0 of disk A:. These files can then be tranferred over to a host computer via XMODEM for real physical printing.
These files are created when the first printing occurs, and will be kept open throughout RunCPM usage. They can be erased inside CP/M to trigger the start of a new printing.
As of now RunCPM does not support printing to physical devices.&lt;/p&gt;
&lt;h3&gt;Limitations / Misbehaviors&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/runcpm/Screenshot_20230819_122029.png" style="width: 640px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: left; padding: 20px 20px 20px 20px;"&gt;&lt;/p&gt;
&lt;p&gt;The objective of RunCPM is &lt;strong&gt;not&lt;/strong&gt; to emulate a Z80 CP/M computer perfectly, but to allow CP/M to be emulated as close as possible while keeping its files on the native (host) filesystem.&lt;/p&gt;
&lt;p&gt;This will obviously prevent the accurate physical emulation of disk drives, so applications like &lt;strong&gt;MOVCPM&lt;/strong&gt; and &lt;strong&gt;STAT&lt;/strong&gt; will not be useful.&lt;/p&gt;
&lt;p&gt;The master disk, A.ZIP, continues to provide the necessary components to maintain compatibility with Digital Research Inc.'s official CP/M distribution. Currently, only CP/M 2.2 is fully supported, though work is ongoing to bring support for CP/M 3.0.&lt;/p&gt;
&lt;p&gt;IN/OUT instructions are designated to facilitate communication between the soft CPU BIOS and BDOS and the equivalent functions within RunCPM, thus these instructions are reserved for this purpose and cannot be used for other tasks. The video monitor in this emulation environment is assumed to be an ANSI/VT100 emulation, which is the standard for DOS/Windows/Linux distributions. This means CP/M applications hard-coded for different terminals may encounter issues with screen rendering.&lt;/p&gt;
&lt;p&gt;When using a serial terminal emulator with RunCPM, it is important to configure the emulator to send either a Carriage Return (CR) or a Line Feed (LF) when the Enter key is pressed, but not both (CR+LF). Sending both can disrupt the DIR listing on Digital Research’s Command Control Processor (CCP), consistent with standard CP/M 2.2 behavior.&lt;/p&gt;
&lt;p&gt;RunCPM does not support setting files to read-only or applying other CP/M-specific file attributes. All files within the RunCPM environment will be both visible and read/write at all times, necessitating careful file handling. RunCPM does support setting "disks" to read-only, but this read-only status applies only within the context of RunCPM. It does not alter the read/write attributes of the disk’s containing folder on the host system.&lt;/p&gt;
&lt;p&gt;Furthermore, some applications, such as Hi-Tech C, may attempt to access user areas numbered higher than 15 to check for a specific CP/M flavor other than 2.2. This action results in the creation of user areas labeled with letters beyond 'F', which is expected behavior and will not be altered in RunCPM.&lt;/p&gt;
&lt;h3&gt;CP/M Software&lt;/h3&gt;
&lt;p&gt;CP/M software library &lt;a href="https://baud.rs/ONPLbs"&gt;here!&lt;/a&gt; or &lt;a href="https://baud.rs/AWtXE7"&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Having inserted the microSD card and connected the Grand Central appropriately, ensuring both board and port settings are accurate, proceed to build and install onto the Grand Central.&lt;/p&gt;
&lt;p&gt;RunCPM provides access to Arduino I/O capabilities through CP/M's BDOS (Basic Disk Operating System) interface. By loading the C register with a function number and a call to address 5, additional functionality that has been added to the system can be accessed. &lt;/p&gt;
&lt;p&gt;For these functions, the number of the pin being used is placed in the D register and the value to write (when appropriate) is placed in E. For read functions, the result is returned as noted.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;PinMode

LD C, 220
LD D, pin_number
LD E, mode ;(0 = INPUT, 1 = OUTPUT, 2 = INPUT_PULLUP)
CALL 5 

DigitalRead 

LD C, 221
LD D, pin_number
CALL 5 Returns result in A (0 = LOW, 1 = HIGH). 

DigitalWrite 

LD C, 222
LD D, pin_number
LD E, value ;(0 = LOW, 1 = HIGH)
CALL 5 

AnalogRead 

LD C, 223
LD D, pin_number
CALL 5 

Returns result in HL (0 - 1023). 

AnalogWrite (i.e. PWM)

LD C, 224
LD D, pin_number
LD E, value (0-255)
CALL 5
Turning on a LED
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Using the provided PinMode and DigitalWrite calls, writing code to control an LED, such as turning it on when connected to pin D8, becomes a straightforward task. To accomplish this, one can use the ED editor to create a file named LED.ASM with the necessary code. This file editing can be done directly on your workstation and saved to the SD card, which is a convenient approach given that ED, the editor, hails from a different era of computing and might feel a bit foreign to modern users accustomed to contemporary text editors.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Turn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;LED&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;wired&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;
&lt;span class="nx"&gt;org&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;
&lt;span class="nx"&gt;mvi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&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;220&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;pinmode&lt;/span&gt;
&lt;span class="nx"&gt;mvi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;digital&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;
&lt;span class="nx"&gt;mvi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;e&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="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;low&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="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;high&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;save&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;arguments&lt;/span&gt;
&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;BDOS&lt;/span&gt;
&lt;span class="nx"&gt;pop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;restore&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;arguments&lt;/span&gt;
&lt;span class="nx"&gt;mvi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&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;222&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;digital&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;write&lt;/span&gt;
&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;BDOS&lt;/span&gt;
&lt;span class="nx"&gt;ret&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;CP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;M&lt;/span&gt;

&lt;span class="nx"&gt;Then&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ASM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;assemble&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

&lt;span class="nx"&gt;A&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;asm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;led&lt;/span&gt;
&lt;span class="nx"&gt;CP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;M&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ASSEMBLER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;VER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m m-Double"&gt;2.0&lt;/span&gt;
&lt;span class="mi"&gt;0111&lt;/span&gt;
&lt;span class="mi"&gt;000&lt;/span&gt;&lt;span class="nx"&gt;H&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;USE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;FACTOR&lt;/span&gt;
&lt;span class="nx"&gt;END&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;OF&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ASSEMBLY&lt;/span&gt;

&lt;span class="nx"&gt;RunCPM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m m-Double"&gt;3.7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;M&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m m-Double"&gt;2.2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This produces several files. &lt;code&gt;LED.PRN&lt;/code&gt; is a text file containing your assembly language program along with the machine code it assembles to. Each line has 3 columns: address, machine code, and assembly language.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nx"&gt;A&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;led&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prn&lt;/span&gt;

&lt;span class="mi"&gt;0100&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nx"&gt;org&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;
&lt;span class="mi"&gt;0100&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="nx"&gt;EDC&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nx"&gt;mvi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;220&lt;/span&gt;
&lt;span class="mi"&gt;0102&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1608&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nx"&gt;mvi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;
&lt;span class="mi"&gt;0104&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;E01&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nx"&gt;mvi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;e&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="mi"&gt;0106&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;D5&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;
&lt;span class="mi"&gt;0107&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;CD0500&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="mi"&gt;010&lt;/span&gt;&lt;span class="nx"&gt;A&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;D1&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="nx"&gt;pop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;
&lt;span class="mi"&gt;010&lt;/span&gt;&lt;span class="nx"&gt;B&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="nx"&gt;EDE&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nx"&gt;mvi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&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;222&lt;/span&gt;
&lt;span class="mi"&gt;010&lt;/span&gt;&lt;span class="nx"&gt;D&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;CD0500&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="mi"&gt;0110&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;C9&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="nx"&gt;ret&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There is also now a LED.HEX file. We can use the LOAD command/program to convert it into &lt;code&gt;LED.COM&lt;/code&gt; which can be executed.&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="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;load&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;led&lt;/span&gt;

&lt;span class="n"&gt;FIRST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ADDRESS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0100&lt;/span&gt;
&lt;span class="n"&gt;LAST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ADDRESS&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mi"&gt;0110&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;READ&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="mi"&gt;0011&lt;/span&gt;
&lt;span class="n"&gt;RECORDS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;WRITTEN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now it can executed:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;A&amp;gt;led&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;which will turn on the LED connected to pin D8.&lt;/p&gt;
&lt;p&gt;So now we can read and write digital and analog I/O from Z80 assembly language code that's running on a Z80 emulated on the Grand Central. That seems pretty round-about.&lt;/p&gt;
&lt;p&gt;While that's true, the point is to be able to play around with Z80 assembly language (and CP/M in this case) without having to find or build an actual Z80 system (although that can be its own kind of fun).&lt;/p&gt;
&lt;h3&gt;Closing Thoughts&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/runcpm/Screenshot_20230819_123543.png" style="width: 640px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: right; padding: 20px 20px 20px 20px;"&gt;&lt;/p&gt;
&lt;p&gt;One of the most lasting legacies of CP/M is its file system and command-line interface, with its 8-character filename followed by a 3-character file type (e.g., &lt;code&gt;filename.txt&lt;/code&gt;), which became a standard that was carried into MS-DOS and later Windows. Its command-line interface, with commands like &lt;code&gt;DIR&lt;/code&gt; to list files and &lt;code&gt;REN&lt;/code&gt; to rename files, has echoes in the MS-DOS command prompt and persists in modern versions of Windows as the Command Prompt and PowerShell. CP/M was notable for being one of the first operating systems that was largely machine-independent, due to its separation between the operating system and the BIOS (Basic Input/Output System). This made it relatively easy to port CP/M to different computer systems and paved the way for the concept of a software ecosystem that is not tied to a specific set of hardware, a key principle in modern operating system design.&lt;/p&gt;
&lt;p&gt;CP/M played a crucial role in the early days of personal computing; before the dominance of MS-DOS and Windows, CP/M was the de facto standard operating system for early microcomputers, fostering the personal computing revolution by making computers more approachable and useful for everyday tasks. When IBM was developing its first personal computer, CP/M was initially considered the operating system of choice, and although IBM ultimately went with MS-DOS (largely due to cost and timing), MS-DOS itself was heavily influenced by CP/M, with many command-line commands being similar and the overall architecture of MS-DOS bearing a strong resemblance to CP/M. This influence extended as MS-DOS evolved into Windows, making CP/M an indirect ancestor of one of the world’s most widely used operating systems. Even after its decline as a primary OS for general-purpose computers, CP/M found a second life in embedded systems and other specialized computing applications due to its lightweight, efficient design, setting the stage for the importance of compact, efficient operating systems in embedded and specialized computing devices, a category that has grown with the proliferation of IoT (Internet of Things) devices. In summary, CP/M stands as an iconic example of how early innovations in computing continue to have ripple effects that extend far into the future.&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://baud.rs/Io0lxk"&gt;https://www.cocoacrumbs.com/blog/2020-08-25-runcpm-on-linux-and-arduino-due/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/1CAEh8"&gt;https://weblambdazero.blogspot.com/2016/07/cpm-on-stick_16.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/RIDXQM"&gt;https://hackaday.io/project/19560-z80-cpm-computer-using-an-arduino&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/JBr4I2"&gt;http://www.chstercius.cz/runcpm/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/8eg6bP"&gt;https://hackaday.com/2014/12/30/z80-cpm-and-fat-file-formats/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/6xoi38"&gt;http://mrwrightteacher.net/retrochallenge2018/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/Ne43qD"&gt;https://learn.adafruit.com/z80-cpm-emulator-for-the-samd51-grand-central&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/0VsV5d"&gt;https://forum.armbian.com/topic/8569-runcpm-on-armbian-cpm-weekend-fun/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/cQe7Oa"&gt;https://blog.hackster.io/z80-cp-m-emulator-runs-on-adafruits-new-grand-central-dev-board-6a28ad73dfbc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/C72vVF"&gt;https://www.instructables.com/Retro-CPM-Stand-Alone-Emulator/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/sZKRnw"&gt;https://www.hackster.io/news/kian-ryan-s-tiny-2040-build-could-be-the-smallest-cp-m-microcomputer-in-history-5d8eeab29d29&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/sYqijE"&gt;https://github.com/ptcryan/CPM_Due&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/7NBs5K"&gt;https://projecthub.arduino.cc/FoxyLab/arduino-nano-as-cpm-compatible-computer-b1300d&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/kiVM0M"&gt;https://dmitryelj.medium.com/the-1979-cp-m-os-how-does-it-look-like-today-be7caf13da6c&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style="width: 100%; text-align: center;"&gt;
&lt;img src="https://tinycomputers.io/images/runcpm/Screenshot_20230818_202355.png" style="min-width: 640px;"&gt;
&lt;img src="https://tinycomputers.io/images/runcpm/Screenshot_20230819_125921.png" style="min-width: 640px;"&gt;
&lt;img src="https://tinycomputers.io/images/runcpm/Screenshot_20230819_130217.png" style="min-width: 640px;"&gt;
&lt;img src="https://tinycomputers.io/images/runcpm/Screenshot_20230819_142558.png" style="min-width: 640px;"&gt;
&lt;/div&gt;</description><category>cp/m</category><category>personal computers</category><category>retro computing</category><guid>https://tinycomputers.io/posts/cpm-history-and-legacy.html</guid><pubDate>Sat, 19 Aug 2023 17:23:47 GMT</pubDate></item></channel></rss>