<?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 cpu design)</title><link>https://tinycomputers.io/</link><description></description><atom:link href="https://tinycomputers.io/categories/cpu-design.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><copyright>Original site content © 2022–2026 Tiny Machines Workshop, LLC, except where otherwise noted. Some rights reserved.</copyright><lastBuildDate>Sat, 29 Aug 2026 23:17:16 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>Part 4: 132 Tests, Zero Failures - Verifying the Sampo CPU on Real Hardware</title><link>https://tinycomputers.io/posts/sampo-fpga-isa-verification.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/sampo-fpga-isa-verification_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;12 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;In &lt;a href="https://tinycomputers.io/posts/sampo-16-bit-risc-cpu-part-1.html"&gt;Part 1&lt;/a&gt;, we designed the Sampo 16-bit RISC architecture. In &lt;a href="https://tinycomputers.io/posts/sampo-fpga-implementation-ulx3s.html"&gt;Part 2&lt;/a&gt;, we synthesized it to an ECP5 FPGA on the ULX3S board. In &lt;a href="https://tinycomputers.io/posts/sampo-llvm-backend-rust-compiler.html"&gt;Part 3&lt;/a&gt;, we built an LLVM backend so Rust could compile for it. But there was a glaring gap in the project: we'd never systematically verified that the hardware actually implements the ISA correctly.&lt;/p&gt;
&lt;p&gt;The "Hello, Sampo!" demo program exercises maybe 10 of the CPU's 66 instructions. The LLVM backend generates code that assumes the hardware matches the spec. If a single instruction is subtly wrong - a carry flag not set, a branch offset miscalculated, a byte load sign-extending when it shouldn't - the entire toolchain is built on sand.&lt;/p&gt;
&lt;p&gt;This post documents the process of building a comprehensive test suite, running it in simulation, finding a real pipeline hazard bug in the CPU, and then the surprisingly treacherous journey of getting those tests running on real FPGA hardware.&lt;/p&gt;
&lt;h3&gt;The Test Strategy&lt;/h3&gt;
&lt;p&gt;The approach is straightforward: write assembly programs that exercise every instruction in the ISA, compare results against known-good values, and report PASS or FAIL over UART. The testbench monitors the serial output, and if it sees "FAIL" anywhere, the test run fails.&lt;/p&gt;
&lt;p&gt;Each test follows the same pattern:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;; Load known inputs&lt;/span&gt;
&lt;span class="nf"&gt;LIX&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;R8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0x1234&lt;/span&gt;
&lt;span class="nf"&gt;LIX&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;R9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0x5678&lt;/span&gt;

&lt;span class="c1"&gt;; Execute the instruction under test&lt;/span&gt;
&lt;span class="nf"&gt;ADD&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;R10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R9&lt;/span&gt;

&lt;span class="c1"&gt;; Check the result&lt;/span&gt;
&lt;span class="nf"&gt;MOV&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;R4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R10&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="c1"&gt;; actual value&lt;/span&gt;
&lt;span class="nf"&gt;LIX&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;R5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0x68AC&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="c1"&gt;; expected value&lt;/span&gt;
&lt;span class="nf"&gt;JALX&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;check_eq&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="c1"&gt;; prints PASS or FAIL&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;check_eq&lt;/code&gt; subroutine compares R4 (actual) against R5 (expected) and prints the result over the UART. This makes the test output human-readable and machine-parseable:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;=== ALU Tests ===
ADD basic: PASS
ADD zero: PASS
ADD carry out: PASS
ADD overflow: PASS
SUB basic: PASS
...
Done.
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;The Test Framework&lt;/h3&gt;
&lt;p&gt;Every test program begins with a block of helper subroutines that handle UART communication and result reporting. The core is a busy-wait loop that polls the MC6850-compatible UART status register:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="na"&gt;.equ&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;ACIA_STATUS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0x80&lt;/span&gt;
&lt;span class="na"&gt;.equ&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;ACIA_DATA&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;0x81&lt;/span&gt;

&lt;span class="nl"&gt;print_char:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;; R5 = character to output&lt;/span&gt;
&lt;span class="nl"&gt;.wait:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nf"&gt;INI&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;R6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;ACIA_STATUS&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;; Read status register&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nf"&gt;AND&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;R7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R6&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="c1"&gt;; Copy to R7&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nf"&gt;ADDI&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R7&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="w"&gt;             &lt;/span&gt;&lt;span class="c1"&gt;; Check if TX ready (bit 1)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nf"&gt;BNE&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;.wait&lt;/span&gt;&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="c1"&gt;; Loop until ready&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nf"&gt;OUTI&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;ACIA_DATA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R5&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c1"&gt;; Send character&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nf"&gt;JR&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="no"&gt;RA&lt;/span&gt;&lt;span class="w"&gt;                 &lt;/span&gt;&lt;span class="c1"&gt;; Return&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;check_eq&lt;/code&gt; helper prints "PASS" or "FAIL" based on a register comparison, and the &lt;code&gt;print_str&lt;/code&gt; helper walks a null-terminated string byte by byte. These routines are duplicated in each test file rather than linked - there's no linker in this toolchain, just a single-file assembler.&lt;/p&gt;
&lt;h3&gt;Test Coverage&lt;/h3&gt;
&lt;p&gt;We organized the tests into 10 programs, each targeting a specific area of the instruction set:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test Program&lt;/th&gt;
&lt;th&gt;Instructions Tested&lt;/th&gt;
&lt;th&gt;Test Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;test_alu&lt;/td&gt;
&lt;td&gt;ADD, SUB, AND, OR, XOR, NEG + flags&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_addi&lt;/td&gt;
&lt;td&gt;ADDI with signed immediates + flags&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_shift&lt;/td&gt;
&lt;td&gt;SLL, SRL, SRA, ROL, ROR, SWAP (1/4/8-bit variants)&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_muldiv&lt;/td&gt;
&lt;td&gt;MUL, MULH, DIV, DIVU, REM, REMU&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_loadstore&lt;/td&gt;
&lt;td&gt;LW, LB, LBU, SW, SB + offset variants&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_branch&lt;/td&gt;
&lt;td&gt;All 16 branch conditions (taken + not taken)&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_jump&lt;/td&gt;
&lt;td&gt;J, JR, JALR, JX, JALX&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_stack&lt;/td&gt;
&lt;td&gt;PUSH, POP, CMP, TEST, MOV&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_misc&lt;/td&gt;
&lt;td&gt;EXX, GETF, SETF, SCF, CCF, NOP&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test_extended&lt;/td&gt;
&lt;td&gt;ADDIX, SUBIX, ANDIX, ORIX, XORIX, SLLX, SRLX, SRAX&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;132&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The branch tests are particularly thorough - each of the 16 conditions (BEQ, BNE, BLT, BGE, BLTU, BGEU, BMI, BPL, BVS, BVC, BCS, BCC, BGT, BLE, BHI, BLS) gets tested both for the taken and not-taken case. We set up flags with arithmetic, then verify the branch goes the right way.&lt;/p&gt;
&lt;h3&gt;Finding a Real Bug: The Pipeline Hazard&lt;/h3&gt;
&lt;p&gt;The first time we ran the full test suite in simulation, 130 of 132 tests passed. Two tests in &lt;code&gt;test_loadstore&lt;/code&gt; were failing: the multi-word store/load test and a load with offset test.&lt;/p&gt;
&lt;p&gt;The failing pattern was consistent: any test that performed a store followed immediately by a load from a different address would read stale data. The load would return the value from the &lt;em&gt;previous&lt;/em&gt; memory operation instead of the current one.&lt;/p&gt;
&lt;p&gt;The root cause was a pipeline hazard between the MEMORY and FETCH states. Here's what was happening:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;Cycle N:   MEMORY state - store completes, mem_ready asserts
Cycle N+1: FETCH state  - new instruction fetch begins
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The problem: &lt;code&gt;mem_ready&lt;/code&gt; is a one-cycle delayed version of &lt;code&gt;mem_valid&lt;/code&gt; (because the RAM is synchronous). When the CPU transitions from MEMORY to WRITEBACK to FETCH, the &lt;code&gt;mem_ready&lt;/code&gt; signal from the store was still asserted during the first cycle of the next FETCH. The CPU latched the stale &lt;code&gt;mem_rdata&lt;/code&gt; from the previous store operation as if it were the new instruction.&lt;/p&gt;
&lt;p&gt;The fix was to add a WRITEBACK state after every MEMORY operation - not just loads, but stores too. This gives &lt;code&gt;mem_ready&lt;/code&gt; a cycle to deassert before the next FETCH begins:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;Before&lt;/span&gt;&lt;span class="o"&gt;:&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="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;FETCH&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mem_ready&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;still&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;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;After&lt;/span&gt;&lt;span class="o"&gt;:&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="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;WRITEBACK&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="n"&gt;FETCH&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mem_ready&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;deasserts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;during&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;WRITEBACK&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;A one-line change to the next-state logic:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="no"&gt;`ST_MEMORY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;begin&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;mem_ready&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;begin&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// Always go through WRITEBACK after MEMORY.&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// For stores: allows mem_ready to deassert before&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// next FETCH (prevents stale rdata latch).&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;next_state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;`ST_WRITEBACK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is exactly the kind of bug that simulation catches and manual inspection misses. The instruction executes correctly in isolation - it's only the &lt;em&gt;interaction&lt;/em&gt; between consecutive memory operations that triggers the hazard. After the fix, all 132 tests passed in simulation.&lt;/p&gt;
&lt;h3&gt;Taking It to the FPGA&lt;/h3&gt;
&lt;p&gt;With simulation clean, the next step was running the tests on real hardware. The ULX3S board has an &lt;a href="https://baud.rs/bJSrEK"&gt;FTDI&lt;/a&gt; FT231X USB-serial chip connected to the FPGA, so UART output appears on a serial port at 115200 baud.&lt;/p&gt;
&lt;p&gt;There was an immediate practical problem: the test programs run fast. At 12.5 MHz, the entire 20-test ALU suite completes in about 30 milliseconds. By the time openFPGALoader finishes programming the FPGA and releases the USB port, the test output is long gone. The FTDI chip has a small receive buffer, but 364 characters of test output overflows it before you can open the serial port.&lt;/p&gt;
&lt;p&gt;The solution: patch the hex files to loop instead of halting. Replace the HALT instruction with a delay loop followed by a jump back to the reset vector. The test runs, outputs its results, waits about half a second, and starts over. You can open the serial port at any time and catch a complete iteration.&lt;/p&gt;
&lt;h4&gt;The Delay Loop Patch&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;hex_loop_patch.py&lt;/code&gt; script performs binary patching on the assembled hex files. It finds the HALT instruction (encoded as &lt;code&gt;0xE100&lt;/code&gt;) and replaces it with a delay loop:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;; Delay ~0.38 seconds at 12.5 MHz&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nf"&gt;LIX&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;R8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0x0008&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="c1"&gt;; outer counter&lt;/span&gt;
&lt;span class="nl"&gt;outer:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nf"&gt;LIX&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;R9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0xFFFF&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="c1"&gt;; inner counter = 65535&lt;/span&gt;
&lt;span class="nl"&gt;inner:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nf"&gt;ADDI&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R9&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="nf"&gt;BNE&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;inner&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nf"&gt;ADDI&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R8&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="nf"&gt;BNE&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;outer&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nf"&gt;JX&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;0x0100&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="c1"&gt;; jump back to reset vector&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The first version of this script &lt;em&gt;inserted&lt;/em&gt; these 10 words at the HALT position. This seemed obviously correct. The tests ran on FPGA. Characters appeared on the serial port.&lt;/p&gt;
&lt;p&gt;They were the wrong characters.&lt;/p&gt;
&lt;h3&gt;The Address Shift Bug&lt;/h3&gt;
&lt;p&gt;The FPGA output for the "Hello, Sampo!" test program was &lt;code&gt;\x08\x08\x08\x08&lt;/code&gt; - four backspace characters, repeating forever. The ALU test suite showed truncated output with roughly 45% of characters missing. Same pattern at 12.5 MHz and 6.25 MHz, ruling out timing violations. Simulation with realistic UART timing (1,080 cycles per byte, matching the hardware baud rate) passed perfectly.&lt;/p&gt;
&lt;p&gt;I spent considerable time investigating the wrong theories. Was the UART transmitter dropping bytes? Was there a clock domain crossing issue? Was &lt;code&gt;$readmemh&lt;/code&gt; in Yosys interpreting the hex file differently from Icarus Verilog? None of these panned out.&lt;/p&gt;
&lt;p&gt;The breakthrough came from staring at &lt;code&gt;\x08&lt;/code&gt;. That's the byte value 8. Where would 8 come from? The "Hello, Sampo!" program loads its message pointer with &lt;code&gt;LIX R4, message&lt;/code&gt; where &lt;code&gt;message&lt;/code&gt; is the label for the string data. In the assembled hex, &lt;code&gt;message&lt;/code&gt; resolves to address &lt;code&gt;0x011E&lt;/code&gt; - the byte immediately after the HALT instruction.&lt;/p&gt;
&lt;p&gt;And there it was. Look at the assembly structure:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nl"&gt;done:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nf"&gt;HALT&lt;/span&gt;&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="c1"&gt;; address 0x011C&lt;/span&gt;
&lt;span class="nl"&gt;message:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;.asciz&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Hello, Sampo!\n"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;; address 0x011E&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The string data lives immediately after HALT. When &lt;code&gt;hex_loop_patch.py&lt;/code&gt; &lt;em&gt;inserts&lt;/em&gt; 10 words of delay loop code at the HALT position, it pushes the string data down by 20 bytes. But the &lt;code&gt;LIX R4, 0x011E&lt;/code&gt; instruction still points to the original address. At &lt;code&gt;0x011E&lt;/code&gt; there's now the second word of &lt;code&gt;LIX R8, 0x0008&lt;/code&gt; - which contains the value &lt;code&gt;0x0008&lt;/code&gt;. The low byte is &lt;code&gt;0x08&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The CPU faithfully reads byte &lt;code&gt;0x08&lt;/code&gt; from the patched address, outputs it via UART, advances the pointer to &lt;code&gt;0x011F&lt;/code&gt; where the high byte is &lt;code&gt;0x00&lt;/code&gt; (the null terminator), and stops. One &lt;code&gt;\x08&lt;/code&gt; per iteration, four iterations captured. Mystery solved.&lt;/p&gt;
&lt;p&gt;This same address shift corrupted every test program. The test strings ("ADD basic: ", "PASS\n", etc.) all live after HALT and all got displaced. The CPU was reading from locations that now contained delay loop machine code instead of ASCII text. Some fragments of text survived because adjacent strings partially overlapped with their shifted locations, producing the truncated output we saw.&lt;/p&gt;
&lt;h4&gt;The Fix&lt;/h4&gt;
&lt;p&gt;The correct approach: don't shift any data. Place the delay loop at address &lt;code&gt;0x0000&lt;/code&gt; - the 256 bytes of unused memory before the &lt;code&gt;0x0100&lt;/code&gt; reset vector - and replace the single-word HALT with a single-word relative &lt;code&gt;J&lt;/code&gt; (jump) instruction that jumps backward to the loop code. One word replaces one word. No data moves.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# Place delay loop at address 0x0000 (unused space)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LOOP_PATCH&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;loop_base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;

&lt;span class="c1"&gt;# Replace HALT with J instruction to address 0x0000&lt;/span&gt;
&lt;span class="c1"&gt;# J encoding: opcode 0x9, 12-bit signed offset&lt;/span&gt;
&lt;span class="c1"&gt;# target = PC + 2 + (sign_extend(offset) &amp;lt;&amp;lt; 1)&lt;/span&gt;
&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_addr&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;halt_addr&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&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;j_word&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x9000&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xFFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;halt_idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;j_word&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There's a subtle complication: the J instruction shares opcode &lt;code&gt;0x9&lt;/code&gt; with JR (register indirect jump) and JALR (jump and link register). The decoder distinguishes them by specific bit patterns in the offset field. If the calculated offset happens to have &lt;code&gt;bits[3:0] == 0x1&lt;/code&gt; and &lt;code&gt;bits[11:8] != 0xF&lt;/code&gt;, the decoder interprets it as JALR instead of J. The script tries successive target addresses (&lt;code&gt;0x0000&lt;/code&gt;, &lt;code&gt;0x0002&lt;/code&gt;, &lt;code&gt;0x0004&lt;/code&gt;, ...) until it finds one that doesn't collide with the JR/JALR encoding space.&lt;/p&gt;
&lt;p&gt;After the fix, the patched hex files have exactly the same number of words as the originals. The only changes are the delay loop code written to the zero page and the HALT word replaced with a backward jump.&lt;/p&gt;
&lt;p&gt;With the corrected patcher, the "Hello, Sampo!" program finally works on the FPGA - looping cleanly with zero character loss:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/sampo-fpga-isa-verification/HelloSampo.png" style="width: 100%; max-width: 720px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); margin: 1em 0;" loading="lazy" alt="Terminal showing Hello, Sampo! repeating on the ULX3S FPGA via cu serial connection"&gt;&lt;/p&gt;
&lt;h3&gt;The Testbench: Trusting but Verifying&lt;/h3&gt;
&lt;p&gt;One important discovery during this process: the simulation testbench had &lt;code&gt;tx_ready = 1&lt;/code&gt; permanently. The simulated UART never pushed back on the CPU - it accepted every byte instantly. This meant the CPU's busy-wait loop (&lt;code&gt;INI R6, ACIA_STATUS / ADDI R7, -2 / BNE wait&lt;/code&gt;) was never actually tested in simulation. The status register always returned "ready," so the loop body executed zero times.&lt;/p&gt;
&lt;p&gt;On real hardware, the UART transmitter takes about 87 microseconds per byte at 115200 baud. The busy-wait loop runs hundreds of times per character, exercising the INI instruction, the AND/ADDI flag-setting sequence, and the BNE branch in a tight loop. If any of those instructions had a subtle bug, it would only manifest on hardware.&lt;/p&gt;
&lt;p&gt;We added realistic UART timing to the testbench:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;parameter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TX_BYTE_CYCLES&lt;/span&gt;&lt;span class="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;108&lt;/span&gt;&lt;span class="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;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// ~1080 cycles per byte&lt;/span&gt;
&lt;span class="kt"&gt;reg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mh"&gt;15&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mh"&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;tx_delay_cnt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;always&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="k"&gt;posedge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;clk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;begin&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;tx_valid&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;tx_ready&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;begin&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;tx_ready&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="mh"&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;tx_delay_cnt&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;TX_BYTE_CYCLES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;end&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;tx_delay_cnt&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;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;begin&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;tx_delay_cnt&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;tx_delay_cnt&lt;/span&gt;&lt;span class="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;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;tx_delay_cnt&lt;/span&gt;&lt;span class="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;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;tx_ready&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="mh"&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;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With this change, simulation exercises the same code paths as the hardware. All 132 tests still pass - the UART flow control logic was correct all along, it just wasn't being tested.&lt;/p&gt;
&lt;h3&gt;Running All Tests on the FPGA&lt;/h3&gt;
&lt;video controls style="width: 100%; max-width: 720px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); margin: 0 0 1em 0;"&gt;
&lt;source src="https://tinycomputers.io/sampo-fpga-test-suite.mp4" type="video/mp4"&gt;
Your browser does not support the video tag.
&lt;/source&gt;&lt;/video&gt;

&lt;p&gt;With the patch bug fixed, we ran the complete suite. Each test requires a separate FPGA build (Yosys synthesis, nextpnr place-and-route, ecppack bitstream generation), programming via JTAG, and serial capture. The Makefile automates the entire pipeline:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nf"&gt;fpga-%&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;BUILD_DIR&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;/&lt;span class="n"&gt;sampo_&lt;/span&gt;%.&lt;span class="n"&gt;bit&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;openFPGALoader&lt;span class="w"&gt; &lt;/span&gt;-b&lt;span class="w"&gt; &lt;/span&gt;ulx3s&lt;span class="w"&gt; &lt;/span&gt;$&amp;lt;
&lt;span class="w"&gt;    &lt;/span&gt;sleep&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;python3&lt;span class="w"&gt; &lt;/span&gt;fpga_capture.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;SERIAL_PORT&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;SERIAL_BAUD&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;fpga_capture.py&lt;/code&gt; script opens the serial port, discards the first partial iteration (we might join mid-stream), waits for the &lt;code&gt;=== ... ===&lt;/code&gt; header line that starts each test, captures everything until the header repeats, and outputs one clean iteration.&lt;/p&gt;
&lt;p&gt;The results:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;========================================
=== FPGA: test_alu ===
========================================
=== ALU Tests ===
ADD basic: PASS
ADD zero: PASS
ADD carry out: PASS
...
AND clr C/V: PASS
All tests passed!

========================================
=== FPGA: test_addi ===
========================================
...
All tests passed!

...

========================================
FPGA Test Summary: 10 passed, 0 failed
========================================
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;All 10 test suites pass. All 132 individual tests pass. Zero failures on real hardware.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test Suite&lt;/th&gt;
&lt;th&gt;Tests&lt;/th&gt;
&lt;th&gt;FPGA Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ALU (ADD, SUB, AND, OR, XOR)&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;All PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ADDI (immediate arithmetic)&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;All PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shift (SLL, SRL, SRA, ROL, SWAP)&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;All PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MulDiv (MUL, DIV, REM variants)&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;All PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load/Store (LW, LB, LBU, SW, SB)&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;All PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Branch (all 16 conditions)&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;All PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jump (J, JR, JALR, JX, JALX)&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;All PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stack (PUSH, POP, CMP, TEST, MOV)&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;All PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Misc (EXX, GETF, SETF, SCF, CCF, NOP)&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;All PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extended (ADDIX, SUBIX, SLLX, etc.)&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;All PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;132&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;All PASS&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;What This Means&lt;/h3&gt;
&lt;p&gt;Having all 132 ISA tests pass on hardware is a significant milestone for the project. It means:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Verilog RTL is correct.&lt;/strong&gt; Every instruction in the Sampo ISA produces the right result, sets the right flags, and handles edge cases (zero, overflow, carry, sign extension) correctly. Not just in behavioral simulation, but in synthesized logic on a real FPGA running at 12.5 MHz.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The assembler is correct.&lt;/strong&gt; All 66 instructions encode properly. Branch offsets calculate correctly. Extended instructions (LIX, JALX, OUTX) with their 32-bit encoding work. The &lt;code&gt;sasm&lt;/code&gt; Rust assembler and the Verilog decoder agree on every instruction format.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The LLVM backend has a solid foundation.&lt;/strong&gt; When the Rust compiler generates a &lt;code&gt;ADD&lt;/code&gt; or &lt;code&gt;BNE&lt;/code&gt; or &lt;code&gt;JALX&lt;/code&gt;, the hardware will execute it correctly. The test suite doesn't exercise every possible code generation pattern, but it validates every primitive instruction that the compiler builds upon.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The UART subsystem works end-to-end.&lt;/strong&gt; Status register polling, TX busy-wait, byte transmission, baud rate generation - all verified on hardware. The MC6850-compatible interface works exactly as specified.&lt;/p&gt;
&lt;h3&gt;Lessons Learned&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Test your assumptions.&lt;/strong&gt; The testbench had &lt;code&gt;tx_ready = 1&lt;/code&gt;. It went unnoticed because simulation "worked." The real hardware exercises code paths that simulation shortcuts. Add realistic peripheral timing to your testbenches from day one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Binary patching is fragile.&lt;/strong&gt; Inserting bytes into a binary without updating references is a classic relocation bug - the same class of problem that linkers exist to solve. If your patch changes the size of anything, every address reference past the patch point is wrong. The fix - placing the patch in unused address space and using a same-size replacement instruction - avoids the problem entirely.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Simulation is necessary but not sufficient.&lt;/strong&gt; The pipeline hazard bug was caught by simulation. The address shift bug was invisible to simulation (both used the same patching script, and the original programs - without patching - worked fine). You need both simulation and hardware testing, exercising different code paths and different failure modes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Systematic testing finds bugs that demos don't.&lt;/strong&gt; "Hello, Sampo!" worked on the FPGA from day one. It exercises &lt;code&gt;LIX&lt;/code&gt;, &lt;code&gt;LBU&lt;/code&gt;, &lt;code&gt;CMP&lt;/code&gt;, &lt;code&gt;BEQ&lt;/code&gt;, &lt;code&gt;INI&lt;/code&gt;, &lt;code&gt;OUTI&lt;/code&gt;, &lt;code&gt;ADDI&lt;/code&gt;, and &lt;code&gt;J&lt;/code&gt; - about 8 instructions. The pipeline hazard only manifested when a store was followed by a load to a different address, a pattern that doesn't occur in a simple print loop. You need tests specifically designed to exercise corner cases.&lt;/p&gt;
&lt;h3&gt;What's Next&lt;/h3&gt;
&lt;p&gt;The entire Sampo project - assembler, emulator, Verilog RTL, FPGA build scripts, test suite, and LLVM backend - is open source on &lt;a href="https://baud.rs/r74wA8"&gt;GitHub&lt;/a&gt;. With hardware verification complete, the next steps might be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Running Rust-compiled code on the FPGA.&lt;/strong&gt; The LLVM backend generates assembly, the assembler produces hex files, and we now know the hardware executes them correctly. Closing this loop - &lt;code&gt;cargo build&lt;/code&gt; to blinking LEDs - is the obvious next milestone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Adding more peripherals.&lt;/strong&gt; The ULX3S has 32MB of SDRAM, an HDMI output, a microSD slot, and an ESP32 co-processor. Each of these opens up interesting possibilities for a working 16-bit computer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance optimization.&lt;/strong&gt; The CPU currently runs at 12.5 MHz with a multi-cycle FSM (5-8 cycles per instruction). Pipelining could push this significantly higher on the ECP5.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But first: 132 tests, zero failures. The Sampo CPU works.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This is Part 4 of the Sampo series. &lt;a href="https://tinycomputers.io/posts/sampo-16-bit-risc-cpu-part-1.html"&gt;Part 1&lt;/a&gt; covers architecture design, &lt;a href="https://tinycomputers.io/posts/sampo-fpga-implementation-ulx3s.html"&gt;Part 2&lt;/a&gt; covers FPGA implementation, and &lt;a href="https://tinycomputers.io/posts/sampo-llvm-backend-rust-compiler.html"&gt;Part 3&lt;/a&gt; covers the LLVM backend.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Recommended Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://baud.rs/wvPosK"&gt;OrangeCrab ECP5 FPGA Board&lt;/a&gt; - A compact Lattice ECP5 board with DDR3 and USB-C, available on Amazon&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/6U3DBr"&gt;ECP5 FPGA Development Boards&lt;/a&gt; - Other ECP5 boards available on Amazon&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/RGjpAj"&gt;&lt;em&gt;Getting Started with FPGAs&lt;/em&gt;&lt;/a&gt; by Russell Merrick - Beginner-friendly introduction with Verilog and VHDL examples&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/bJSrEK"&gt;FTDI USB Serial Adapters&lt;/a&gt; - Useful for UART debugging with FPGAs&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/dBX5Ij"&gt;USB Logic Analyzers&lt;/a&gt; - Essential for debugging digital signals&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Source Code&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://baud.rs/r74wA8"&gt;github.com/ajokela/sampo&lt;/a&gt;&lt;/strong&gt; - CPU architecture, assembler, emulator, Verilog RTL, test suite, and FPGA build scripts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://baud.rs/GCQDRa"&gt;github.com/ajokela/llvm-sampo&lt;/a&gt;&lt;/strong&gt; - LLVM backend and Rust target specification&lt;/li&gt;
&lt;/ul&gt;</description><category>cpu design</category><category>ecp5</category><category>fpga</category><category>hardware</category><category>isa</category><category>risc</category><category>sampo</category><category>testing</category><category>uart</category><category>ulx3s</category><category>verification</category><category>verilog</category><guid>https://tinycomputers.io/posts/sampo-fpga-isa-verification.html</guid><pubDate>Sun, 15 Feb 2026 20:00:00 GMT</pubDate></item><item><title>Part 2: Implementing Sampo on the ULX3S FPGA</title><link>https://tinycomputers.io/posts/sampo-fpga-implementation-ulx3s.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;p&gt;After designing the &lt;a href="https://tinycomputers.io/posts/sampo-16-bit-risc-cpu-part-1.html"&gt;Sampo RISC architecture&lt;/a&gt; on paper (complete with a working assembler and emulator) it's time to bring it to life in silicon. Or at least, in programmable logic. This post documents the hardware selection and implementation planning for synthesizing Sampo on an FPGA.&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/sampo-fpga-implementation-ulx3s_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;The Story So Far&lt;/h3&gt;
&lt;p&gt;If you haven't read &lt;a href="https://tinycomputers.io/posts/sampo-16-bit-risc-cpu-part-1.html"&gt;Part 1 of this series&lt;/a&gt;, here's the quick version: Sampo is a 16-bit RISC CPU designed to bridge the gap between clean RISC design principles and Z80-friendly features. It has 16 general-purpose registers, ~66 instructions, port-based I/O, block operations (LDIR, LDDR), alternate registers for fast interrupt handling, and hardware multiply/divide.&lt;/p&gt;
&lt;p&gt;The project already includes working tools written in Rust:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;sasm&lt;/strong&gt; - A full assembler&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;semu&lt;/strong&gt; - An emulator with TUI debugger (step, breakpoints, memory inspection)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And for hardware implementation, we now have two complete RTL implementations:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Amaranth HDL&lt;/strong&gt; (&lt;code&gt;/rtl/&lt;/code&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cpu.py&lt;/code&gt;, &lt;code&gt;alu.py&lt;/code&gt;, &lt;code&gt;decode.py&lt;/code&gt;, &lt;code&gt;regfile.py&lt;/code&gt;, &lt;code&gt;soc.py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Python-based, excellent for rapid iteration&lt;/li&gt;
&lt;li&gt;Generates Verilog for synthesis&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;AI Assisted Hand-written Verilog&lt;/strong&gt; (&lt;code&gt;/verilog/rtl/&lt;/code&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cpu.v&lt;/code&gt;, &lt;code&gt;alu.v&lt;/code&gt;, &lt;code&gt;decode.v&lt;/code&gt;, &lt;code&gt;regfile.v&lt;/code&gt;, &lt;code&gt;shifter.v&lt;/code&gt;, &lt;code&gt;uart.v&lt;/code&gt;, &lt;code&gt;ram.v&lt;/code&gt;, &lt;code&gt;soc.v&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Readable, portable, works with any toolchain&lt;/li&gt;
&lt;li&gt;Includes testbenches for Icarus Verilog and Verilator&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now it's time to synthesize it to real hardware.&lt;/p&gt;
&lt;h3&gt;Choosing an FPGA Platform&lt;/h3&gt;
&lt;p&gt;The FPGA world is split between proprietary toolchains (Xilinx Vivado, Intel Quartus) and the growing open source ecosystem. For a project like Sampo, where understanding every layer of the stack matters, open source tooling is the clear choice.&lt;/p&gt;
&lt;h4&gt;Open Source FPGA Options&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;FPGA Family&lt;/th&gt;
&lt;th&gt;Capacity&lt;/th&gt;
&lt;th&gt;Toolchain&lt;/th&gt;
&lt;th&gt;Maturity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gowin GW1N/GW2A&lt;/td&gt;
&lt;td&gt;1K-55K LUTs&lt;/td&gt;
&lt;td&gt;Project Apicula&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lattice iCE40&lt;/td&gt;
&lt;td&gt;1K-8K LUTs&lt;/td&gt;
&lt;td&gt;Project IceStorm&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lattice ECP5&lt;/td&gt;
&lt;td&gt;12K-85K LUTs&lt;/td&gt;
&lt;td&gt;Project Trellis&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Xilinx 7-series&lt;/td&gt;
&lt;td&gt;10K-200K+ LUTs&lt;/td&gt;
&lt;td&gt;Project X-Ray (partial)&lt;/td&gt;
&lt;td&gt;Experimental&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;For Sampo, which estimates at &lt;strong&gt;~1,500-2,500 LUTs&lt;/strong&gt; for the basic CPU, even the smaller FPGAs have more than enough capacity. But if we want room to grow (adding caches, more peripherals, maybe even multi-core experiments) a larger device makes sense.&lt;/p&gt;
&lt;h3&gt;The ULX3S Board&lt;/h3&gt;
&lt;p&gt;The &lt;a href="https://baud.rs/Ij7oaR"&gt;ULX3S&lt;/a&gt; is an open hardware development board built around the ECP5 FPGA. It's designed by &lt;a href="https://baud.rs/v9aiPd"&gt;Radiona.org&lt;/a&gt; and has become the de facto standard for open source FPGA development.&lt;/p&gt;
&lt;h4&gt;Specifications&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Specification&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FPGA&lt;/td&gt;
&lt;td&gt;Lattice ECP5 (LFE5U-85F/45F/12F-6BG381C)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LUTs&lt;/td&gt;
&lt;td&gt;12K / 44K / 84K (depending on variant)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USB&lt;/td&gt;
&lt;td&gt;FTDI FT231XS (500 kbit JTAG, 3 Mbit serial)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPIO&lt;/td&gt;
&lt;td&gt;56 pins (28 differential pairs), PMOD-compatible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAM&lt;/td&gt;
&lt;td&gt;32 MB SDRAM @ 166 MHz&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flash&lt;/td&gt;
&lt;td&gt;4-16 MB Quad-SPI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;microSD slot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LEDs&lt;/td&gt;
&lt;td&gt;11 total (8 user, 2 USB, 1 WiFi)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Buttons&lt;/td&gt;
&lt;td&gt;7 (4 direction, 2 fire, 1 power)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audio&lt;/td&gt;
&lt;td&gt;3.5mm jack (stereo + digital/composite)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video&lt;/td&gt;
&lt;td&gt;GPDI (HDMI-compatible) with level shifter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Display&lt;/td&gt;
&lt;td&gt;Header for 0.96" SPI OLED (SSD1331)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wireless&lt;/td&gt;
&lt;td&gt;ESP32-WROOM-32 (WiFi/Bluetooth, standalone JTAG)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ADC&lt;/td&gt;
&lt;td&gt;8 channels, 12-bit, 1 MS/s (MAX11125)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clock&lt;/td&gt;
&lt;td&gt;25 MHz onboard, differential input available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Power&lt;/td&gt;
&lt;td&gt;3 switching regulators (1.1V, 2.5V, 3.3V)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sleep&lt;/td&gt;
&lt;td&gt;5 µA standby, RTC wake-up with battery backup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dimensions&lt;/td&gt;
&lt;td&gt;94mm × 51mm&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;Why ULX3S for Sampo&lt;/h4&gt;
&lt;p&gt;The ULX3S isn't just an FPGA breakout board; it's a complete system:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;32MB SDRAM&lt;/strong&gt;: Real memory, not just block RAM. Essential for running actual programs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HDMI output&lt;/strong&gt;: Video terminal without external hardware.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;microSD slot&lt;/strong&gt;: Load programs, implement a filesystem.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ESP32 co-processor&lt;/strong&gt;: WiFi-based JTAG debugging from any device.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Buttons and LEDs&lt;/strong&gt;: Instant I/O for testing without wiring anything.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Audio output&lt;/strong&gt;: Even supports composite video through the audio jack.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Budget Alternative: Tang Nano 9K&lt;/h3&gt;
&lt;p&gt;Before we dive into the ULX3S, it's worth mentioning a much cheaper option. The &lt;strong&gt;Tang Nano 9K&lt;/strong&gt; (~$15 on AliExpress) uses a Gowin GW1NR-9 FPGA with 8,640 LUTs, more than enough for Sampo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;8,640 LUTs&lt;/li&gt;
&lt;li&gt;64Mbit PSRAM (can serve as the full 64KB address space and then some)&lt;/li&gt;
&lt;li&gt;HDMI output for a video terminal&lt;/li&gt;
&lt;li&gt;USB-C programming&lt;/li&gt;
&lt;li&gt;Fully supported by open-source toolchain (Yosys + nextpnr-gowin)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For initial development and testing, the Tang Nano 9K is hard to beat on price. But the ULX3S offers more I/O, more RAM, and a richer peripheral set, making it the better choice for a more complete Sampo system.&lt;/p&gt;
&lt;h3&gt;LUT Budget Planning&lt;/h3&gt;
&lt;p&gt;The Sampo RTL implementation is designed to be compact. Here's the resource breakdown:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Estimated LUTs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;16 × 16-bit registers&lt;/td&gt;
&lt;td&gt;~256 FFs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ALU (16-bit)&lt;/td&gt;
&lt;td&gt;200 - 400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Control logic&lt;/td&gt;
&lt;td&gt;500 - 1,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instruction decode&lt;/td&gt;
&lt;td&gt;300 - 500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sampo CPU core&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~1,500 - 2,500&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UART (115200 baud)&lt;/td&gt;
&lt;td&gt;200 - 300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SPI controller (SD card)&lt;/td&gt;
&lt;td&gt;300 - 500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPIO controller&lt;/td&gt;
&lt;td&gt;200 - 400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Basic system&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~2,500 - 4,000&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SDRAM controller&lt;/td&gt;
&lt;td&gt;500 - 1,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instruction cache&lt;/td&gt;
&lt;td&gt;1,000 - 2,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data cache&lt;/td&gt;
&lt;td&gt;1,000 - 2,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Full system&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~6,000 - 10,000&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;These estimates are based on typical RISC CPU implementations. The actual numbers will depend on optimization choices and synthesis settings.&lt;/p&gt;
&lt;h4&gt;Variant Recommendations&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;12K LUTs&lt;/strong&gt; (ULX3S-12F): Plenty for basic Sampo + peripherals, tight for caches.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;45K LUTs&lt;/strong&gt; (ULX3S-45F): Comfortable. Full CPU with cache, room for experiments.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;85K LUTs&lt;/strong&gt; (ULX3S-85F): Luxurious. Multi-core experiments, extensive peripherals.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Toolchain Setup&lt;/h3&gt;
&lt;p&gt;The ECP5 toolchain is fully open source:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# macOS (Homebrew)&lt;/span&gt;
brew&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;yosys&lt;span class="w"&gt; &lt;/span&gt;nextpnr-ecp5&lt;span class="w"&gt; &lt;/span&gt;ecpprog&lt;span class="w"&gt; &lt;/span&gt;fujprog

&lt;span class="c1"&gt;# Ubuntu/Debian&lt;/span&gt;
apt&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;yosys&lt;span class="w"&gt; &lt;/span&gt;nextpnr-ecp5&lt;span class="w"&gt; &lt;/span&gt;ecpprog

&lt;span class="c1"&gt;# Amaranth HDL (for our existing RTL)&lt;/span&gt;
pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;amaranth&lt;span class="w"&gt; &lt;/span&gt;amaranth-boards

&lt;span class="c1"&gt;# Or build FPGA tools from source for latest features&lt;/span&gt;
git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://github.com/YosysHQ/yosys
git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://github.com/YosysHQ/nextpnr
git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://github.com/YosysHQ/prjtrellis
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Tool Roles&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&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;strong&gt;Amaranth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Python-based HDL (generates Verilog)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Yosys&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Verilog synthesis (RTL → netlist)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;nextpnr-ecp5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Place and route (netlist → bitstream)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Project Trellis&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ECP5 bitstream documentation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ecpprog/fujprog&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Upload bitstream to board&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;Amaranth Build Flow&lt;/h4&gt;
&lt;p&gt;Since Sampo's RTL is written in Amaranth, the build flow starts with Python:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# Generate Verilog from Amaranth&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rtl/
python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;amaranth&lt;span class="w"&gt; &lt;/span&gt;generate&lt;span class="w"&gt; &lt;/span&gt;soc.py&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;sampo.v

&lt;span class="c1"&gt;# Then synthesize with standard tools&lt;/span&gt;
yosys&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"synth_ecp5 -top sampo_soc -json sampo.json"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sampo.v
nextpnr-ecp5&lt;span class="w"&gt; &lt;/span&gt;--85k&lt;span class="w"&gt; &lt;/span&gt;--package&lt;span class="w"&gt; &lt;/span&gt;CABGA381&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--lpf&lt;span class="w"&gt; &lt;/span&gt;ulx3s.lpf&lt;span class="w"&gt; &lt;/span&gt;--json&lt;span class="w"&gt; &lt;/span&gt;sampo.json&lt;span class="w"&gt; &lt;/span&gt;--textcfg&lt;span class="w"&gt; &lt;/span&gt;sampo.config
ecppack&lt;span class="w"&gt; &lt;/span&gt;sampo.config&lt;span class="w"&gt; &lt;/span&gt;sampo.bit

&lt;span class="c1"&gt;# Program the board&lt;/span&gt;
fujprog&lt;span class="w"&gt; &lt;/span&gt;sampo.bit
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Hand-Written Verilog Implementation&lt;/h4&gt;
&lt;p&gt;In addition to the Amaranth RTL, we now have a complete ai-assisted hand-written Verilog implementation at &lt;code&gt;/verilog/&lt;/code&gt;. While Amaranth can generate Verilog, the auto-generated output isn't particularly readable. The hand-written version is designed for clarity and portability:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;verilog&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="n"&gt;rtl&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="err"&gt;├──&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sampo_pkg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vh&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;# Opcodes, constants, state definitions&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="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;alu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="c1"&gt;# 16-bit ALU with all operations&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="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shifter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c1"&gt;# Barrel shifter (1/4/8-bit shifts, rotates)&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="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;regfile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c1"&gt;# 16 registers + alternate set (EXX)&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="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="c1"&gt;# Instruction decoder&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="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cpu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="c1"&gt;# FSM-based CPU core (8 states)&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="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ram&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="c1"&gt;# 64KB synchronous RAM&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="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;uart&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="c1"&gt;# Simple UART for serial I/O&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="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;soc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="c1"&gt;# Top-level SoC integration&lt;/span&gt;
&lt;span class="err"&gt;├──&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tb&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="err"&gt;├──&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;alu_tb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="c1"&gt;# ALU unit tests&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="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;regfile_tb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;# Register file tests&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="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sampo_tb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="c1"&gt;# Full system testbench&lt;/span&gt;
&lt;span class="err"&gt;├──&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="err"&gt;└──&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hello&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hex&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c1"&gt;# Test program in Verilog hex format&lt;/span&gt;
&lt;span class="err"&gt;├──&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Makefile&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="c1"&gt;# Build automation&lt;/span&gt;
&lt;span class="err"&gt;└──&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bin2hex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="c1"&gt;# Convert sasm output to Verilog $readmemh format&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The Verilog implementation uses an 8-state FSM for the CPU: RESET → FETCH → FETCH_EXT → DECODE → EXECUTE → MEMORY → WRITEBACK → HALTED. This makes timing predictable and debugging straightforward.&lt;/p&gt;
&lt;h4&gt;Simulation with Icarus Verilog&lt;/h4&gt;
&lt;p&gt;The Verilog implementation includes a complete Makefile for testing:&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;verilog/

&lt;span class="c1"&gt;# Run the main simulation (hello world)&lt;/span&gt;
make&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;test&lt;/span&gt;

&lt;span class="c1"&gt;# Run ALU unit tests&lt;/span&gt;
make&lt;span class="w"&gt; &lt;/span&gt;test-alu

&lt;span class="c1"&gt;# Run register file tests&lt;/span&gt;
make&lt;span class="w"&gt; &lt;/span&gt;test-regfile

&lt;span class="c1"&gt;# Build with Verilator (faster simulation)&lt;/span&gt;
make&lt;span class="w"&gt; &lt;/span&gt;verilate

&lt;span class="c1"&gt;# View waveforms in GTKWave&lt;/span&gt;
make&lt;span class="w"&gt; &lt;/span&gt;wave
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Sample output from &lt;code&gt;make test&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c"&gt;=== Sampo CPU Testbench ===&lt;/span&gt;
&lt;span class="c"&gt;RAM init file: &lt;/span&gt;&lt;span class="nt"&gt;..&lt;/span&gt;&lt;span class="c"&gt;/programs/hello&lt;/span&gt;&lt;span class="nt"&gt;.&lt;/span&gt;&lt;span class="c"&gt;hex&lt;/span&gt;

&lt;span class="c"&gt;CPU started at PC=0x0100&lt;/span&gt;
&lt;span class="c"&gt;UART output:&lt;/span&gt;
&lt;span class="nb"&gt;----------------------------------------&lt;/span&gt;
&lt;span class="c"&gt;Hello&lt;/span&gt;&lt;span class="nt"&gt;,&lt;/span&gt;&lt;span class="c"&gt; Sampo!&lt;/span&gt;
&lt;span class="nb"&gt;----------------------------------------&lt;/span&gt;

&lt;span class="c"&gt;Simulation complete:&lt;/span&gt;
&lt;span class="c"&gt;  Final PC:    0x011E&lt;/span&gt;
&lt;span class="c"&gt;  Cycles:      847&lt;/span&gt;
&lt;span class="c"&gt;  UART chars:  14&lt;/span&gt;
&lt;span class="c"&gt;  Status:      HALTED&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The Verilog version is portable to any FPGA toolchain (Xilinx, Intel, Lattice, Gowin) without requiring Amaranth or Python in the build chain.&lt;/p&gt;
&lt;h3&gt;Implementation Roadmap&lt;/h3&gt;
&lt;p&gt;With both Amaranth and Verilog implementations complete and tested in simulation, the roadmap is now about bringing them up on hardware.&lt;/p&gt;
&lt;h4&gt;Phase 1: Core Bring-up ✓ (Complete)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;✓ Instruction fetch and decode&lt;/li&gt;
&lt;li&gt;✓ ALU operations (all 16 operations)&lt;/li&gt;
&lt;li&gt;✓ Barrel shifter (1/4/8-bit shifts, rotates, RCL/RCR)&lt;/li&gt;
&lt;li&gt;✓ Register file with alternate set (EXX)&lt;/li&gt;
&lt;li&gt;✓ FSM-based CPU core (8 states)&lt;/li&gt;
&lt;li&gt;✓ RAM interface (64KB)&lt;/li&gt;
&lt;li&gt;✓ UART for serial I/O&lt;/li&gt;
&lt;li&gt;✓ SoC integration&lt;/li&gt;
&lt;li&gt;✓ Testbenches passing (ALU, regfile, full system)&lt;/li&gt;
&lt;li&gt;✓ Hello World runs in simulation&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Phase 1.5: FPGA Bring-up (Current)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;○ ULX3S pin constraints (.lpf file)&lt;/li&gt;
&lt;li&gt;○ Clock setup (PLL from 25MHz)&lt;/li&gt;
&lt;li&gt;○ Map UART to FTDI&lt;/li&gt;
&lt;li&gt;○ LED heartbeat / debug outputs&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Phase 2: Memory System&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;SDRAM controller for 32MB RAM&lt;/li&gt;
&lt;li&gt;Instruction cache (optional but helps timing)&lt;/li&gt;
&lt;li&gt;Basic interrupt handling&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Phase 3: Peripherals&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;SPI controller for SD card boot&lt;/li&gt;
&lt;li&gt;GPIO controller (buttons, LEDs)&lt;/li&gt;
&lt;li&gt;Timer/counter module&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Phase 4: Advanced Features&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Data cache&lt;/li&gt;
&lt;li&gt;MMU for memory protection&lt;/li&gt;
&lt;li&gt;HDMI text console (VGA timing → GPDI)&lt;/li&gt;
&lt;li&gt;ESP32 WiFi integration for wireless debugging&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Recommended Tools &amp;amp; Books&lt;/h3&gt;
&lt;h4&gt;Hardware&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://baud.rs/HBq3zf"&gt;Tang Nano 9K FPGA&lt;/a&gt; - Budget-friendly FPGA board (~$25 on Amazon, ~$15 on AliExpress)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/BYIR58"&gt;USB Logic Analyzer&lt;/a&gt; - Essential for debugging signals (24MHz, 8 channels)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Books&lt;/h4&gt;
&lt;p&gt;If you're new to Verilog or FPGA development, these are excellent starting points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://baud.rs/RGjpAj"&gt;&lt;em&gt;Getting Started with FPGAs&lt;/em&gt;&lt;/a&gt; by Russell Merrick - Beginner-friendly with Verilog and VHDL examples&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/tEyX95"&gt;&lt;em&gt;Programming FPGAs: Getting Started with Verilog&lt;/em&gt;&lt;/a&gt; by Simon Monk - Practical hands-on guide&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/6qfzvC"&gt;&lt;em&gt;Verilog by Example&lt;/em&gt;&lt;/a&gt; by Blaine Readler - Concise reference for working engineers&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://baud.rs/VQxLTd"&gt;Sampo on GitHub&lt;/a&gt; - Full source including assembler, emulator, and RTL&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/JUjA8C"&gt;ULX3S GitHub&lt;/a&gt; - Schematics, examples, documentation&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/JLKZBr"&gt;Project Trellis&lt;/a&gt; - ECP5 bitstream documentation&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/0QCVAC"&gt;Amaranth HDL&lt;/a&gt; - Python-based hardware description&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/xlX31y"&gt;nextpnr&lt;/a&gt; - Place and route tool&lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/LZdP4F"&gt;Yosys&lt;/a&gt; - Verilog synthesis&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Where to Buy&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;ULX3S:&lt;/strong&gt;
- &lt;a href="https://baud.rs/NClAGd"&gt;AliExpress&lt;/a&gt; - ~$100-150 depending on variant
- &lt;a href="https://baud.rs/AQB0Xg"&gt;Mouser&lt;/a&gt; - Official distribution
- &lt;a href="https://baud.rs/0gTuW6"&gt;CrowdSupply&lt;/a&gt; - Original campaign page&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tang Nano 9K (budget alternative):&lt;/strong&gt;
- &lt;a href="https://baud.rs/HBq3zf"&gt;Amazon&lt;/a&gt; - ~$25, faster shipping
- &lt;a href="https://baud.rs/9G7KR0"&gt;AliExpress&lt;/a&gt; - ~$15, slower shipping&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Next up: Getting our first instructions executing on real hardware. Both the Amaranth and Verilog implementations are ready and tested; Hello World runs in simulation and the testbenches pass. Now it's a matter of pin constraints, clock domains, and debugging the inevitable timing issues.&lt;/p&gt;</description><category>amaranth</category><category>cpu design</category><category>ecp5</category><category>fpga</category><category>hardware</category><category>lattice</category><category>open-source</category><category>risc</category><category>sampo</category><category>ulx3s</category><category>verilog</category><guid>https://tinycomputers.io/posts/sampo-fpga-implementation-ulx3s.html</guid><pubDate>Mon, 02 Feb 2026 18:00:00 GMT</pubDate></item><item><title>Sampo: Designing a 16-bit RISC CPU from Scratch - Part 1: Theory and Architecture</title><link>https://tinycomputers.io/posts/sampo-16-bit-risc-cpu-part-1.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 Finnish mythology, the &lt;strong&gt;Sampo&lt;/strong&gt; is a magical artifact from the epic poem &lt;a href="https://baud.rs/ZEIjqv"&gt;&lt;em&gt;Kalevala&lt;/em&gt;&lt;/a&gt;, compiled by Elias Lönnrot in 1835. According to legend, the Sampo was forged by Ilmarinen, a legendary blacksmith and sky god, from a swan's feather, a grain of barley, a ball of wool, a drop of milk, and a shaft of a distaff. The resulting creation took the form of a magical mill that could produce flour, salt, and gold endlessly, bringing riches and good fortune to its holder.&lt;/p&gt;
&lt;p&gt;The exact nature of the Sampo has been debated by scholars since 1818, with over 30 theories proposed, ranging from a world pillar to an astrolabe to a decorated shield. This mystery makes it a fitting namesake for a CPU architecture: something that transforms simple inputs into useful outputs, whose inner workings invite exploration and understanding.&lt;/p&gt;
&lt;p&gt;This is the first part of a two-part series exploring the Sampo CPU architecture. In this article, we'll dive deep into the theory, design philosophy, and architectural decisions that shaped Sampo. In Part 2, we'll get our hands dirty with an actual FPGA implementation using Amaranth HDL, bringing this processor to life on real silicon.&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/sampo-16-bit-risc-cpu-part-1_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;h3&gt;The Problem Space: Why Another CPU?&lt;/h3&gt;
&lt;p&gt;Before diving into Sampo's architecture, it's worth asking: why design a new CPU at all? The retrocomputing community has no shortage of classic processors to explore (the &lt;a href="https://baud.rs/Ch4htI"&gt;Z80&lt;/a&gt;, &lt;a href="https://baud.rs/CrdDFR"&gt;6502&lt;/a&gt;, 68000) and modern RISC architectures like &lt;a href="https://baud.rs/q9i1Th"&gt;RISC-V&lt;/a&gt; offer clean, well-documented designs for educational purposes.&lt;/p&gt;
&lt;p&gt;The answer lies in a specific niche that existing architectures don't quite fill. Consider the typical workloads of classic 8-bit systems: interpreters for languages like BASIC and Forth, operating systems like CP/M, text editors, and simple games. These workloads have distinct characteristics:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Heavy use of memory operations&lt;/strong&gt;: Block copies, string manipulation, memory fills&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Port-based I/O&lt;/strong&gt;: Serial terminals, disk controllers, sound chips accessed via dedicated I/O instructions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context switching&lt;/strong&gt;: Interrupt handlers that need to save and restore register state quickly&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BCD arithmetic&lt;/strong&gt;: Calculator applications, financial software&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The &lt;a href="https://baud.rs/n39HUo"&gt;Z80 excels&lt;/a&gt; at these tasks through specialized instructions (LDIR, LDDR, IN, OUT) and its alternate register set. But the Z80 is an 8-bit CISC processor with irregular encoding, complex addressing modes, and over 300 instruction variants. This makes it challenging to implement efficiently in modern hardware or to target with optimizing compilers.&lt;/p&gt;
&lt;p&gt;Modern RISC architectures like RISC-V take the opposite approach: clean, orthogonal instruction sets optimized for pipelining and compiler code generation. But they typically use memory-mapped I/O (no dedicated I/O instructions), lack block operations, and provide no alternate register sets for fast context switching.&lt;/p&gt;
&lt;p&gt;Sampo occupies the middle ground, a "Z80 programmer's RISC" that combines the regularity and simplicity of RISC design with the specialized capabilities that made the Z80 so effective for its target workloads.&lt;/p&gt;
&lt;h3&gt;Design Goals&lt;/h3&gt;
&lt;p&gt;Sampo was designed with five primary goals:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;RISC-inspired instruction set&lt;/strong&gt;: Clean, orthogonal design with predictable encoding&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;16-bit native word size&lt;/strong&gt;: Registers, ALU, and memory addressing all 16-bit&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Efficient for interpreters and compilers&lt;/strong&gt;: Stack operations, indirect addressing, hardware multiply/divide&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Simple to implement&lt;/strong&gt;: Suitable for &lt;a href="https://baud.rs/kTjxm9"&gt;FPGA synthesis&lt;/a&gt; or software emulation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Z80-workload compatible&lt;/strong&gt;: Port-based I/O, BCD support, block operations, alternate registers&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These goals create natural tensions. RISC purity would eliminate block operations and port-based I/O. Maximum Z80 compatibility would preserve its irregular encoding. Sampo resolves these tensions by borrowing selectively from multiple architectural traditions.&lt;/p&gt;
&lt;h3&gt;Architectural Lineage&lt;/h3&gt;
&lt;p&gt;Sampo's design draws from four distinct sources, each contributing specific elements:&lt;/p&gt;
&lt;h4&gt;From RISC-V&lt;/h4&gt;
&lt;p&gt;RISC-V's influence is most visible in Sampo's register conventions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Zero register (R0)&lt;/strong&gt;: A register that always reads as zero and ignores writes. This eliminates the need for separate "clear" or "load zero" instructions: &lt;code&gt;ADD R4, R0, R0&lt;/code&gt; clears R4, &lt;code&gt;ADD R4, R5, R0&lt;/code&gt; copies R5 to R4.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Register naming conventions&lt;/strong&gt;: Return address (RA), stack pointer (SP), global pointer (GP), argument registers (A0-A3), temporaries (T0-T3), and saved registers (S0-S3).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Load/store architecture&lt;/strong&gt;: Only load and store instructions access memory; all computation occurs between registers.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;From &lt;a href="https://baud.rs/RGYV4g"&gt;MIPS&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;MIPS contributed Sampo's approach to instruction encoding:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Simple, orthogonal formats&lt;/strong&gt;: A small number of instruction formats (R, I, S, B, J) with consistent field positions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;4-bit primary opcode&lt;/strong&gt;: Sixteen instruction categories, each with function codes for variants&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PC-relative branching&lt;/strong&gt;: Branch targets specified as signed offsets from the program counter&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;From &lt;a href="https://baud.rs/p4IsMu"&gt;ARM Thumb/Thumb-2&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;ARM's Thumb instruction set inspired Sampo's hybrid encoding strategy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;16-bit base instruction width&lt;/strong&gt;: Most common operations fit in 16 bits for improved code density&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;32-bit extended forms&lt;/strong&gt;: Operations requiring larger immediates use a two-word format&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prefix-based extension&lt;/strong&gt;: The 0xF opcode prefix indicates a 32-bit instruction, simplifying decode&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;From the Z80&lt;/h4&gt;
&lt;p&gt;The Z80 provides Sampo's "personality": the features that make it feel familiar to retrocomputing enthusiasts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Port-based I/O&lt;/strong&gt;: IN and OUT instructions with 8-bit port addresses, separate from the memory address space&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Alternate register set&lt;/strong&gt;: The EXX instruction swaps working registers with shadow copies for fast interrupt handling&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Block operations&lt;/strong&gt;: LDIR, LDDR, FILL, and CPIR for efficient memory manipulation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BCD support&lt;/strong&gt;: The DAA (Decimal Adjust Accumulator) instruction for binary-coded decimal arithmetic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;64KB address space&lt;/strong&gt;: 16-bit addresses, matching the Z80's memory model&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The Register File&lt;/h3&gt;
&lt;p&gt;Sampo provides 16 general-purpose 16-bit registers, organized with RISC-V-style conventions:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Register&lt;/th&gt;
&lt;th&gt;Alias&lt;/th&gt;
&lt;th&gt;Convention&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;R0&lt;/td&gt;
&lt;td&gt;ZERO&lt;/td&gt;
&lt;td&gt;Always reads as 0, writes ignored&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R1&lt;/td&gt;
&lt;td&gt;RA&lt;/td&gt;
&lt;td&gt;Return address (saved by caller)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R2&lt;/td&gt;
&lt;td&gt;SP&lt;/td&gt;
&lt;td&gt;Stack pointer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R3&lt;/td&gt;
&lt;td&gt;GP&lt;/td&gt;
&lt;td&gt;Global pointer (optional)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R4-R7&lt;/td&gt;
&lt;td&gt;A0-A3&lt;/td&gt;
&lt;td&gt;Arguments / Return values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R8-R11&lt;/td&gt;
&lt;td&gt;T0-T3&lt;/td&gt;
&lt;td&gt;Temporaries (caller-saved)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R12-R15&lt;/td&gt;
&lt;td&gt;S0-S3&lt;/td&gt;
&lt;td&gt;Saved registers (callee-saved)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The zero register deserves special attention. Having a register that always contains zero eliminates entire classes of instructions found in other architectures:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MOV Rd, Rs&lt;/strong&gt; becomes &lt;code&gt;ADD Rd, Rs, R0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CLR Rd&lt;/strong&gt; becomes &lt;code&gt;ADD Rd, R0, R0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NEG Rd, Rs&lt;/strong&gt; can use R0 as the implicit minuend&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CMP Rs, #0&lt;/strong&gt; becomes &lt;code&gt;SUB R0, Rs, R0&lt;/code&gt; (result discarded, flags set)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This technique, pioneered by MIPS and refined by RISC-V, dramatically simplifies the instruction set while maintaining expressiveness.&lt;/p&gt;
&lt;h4&gt;Alternate Registers&lt;/h4&gt;
&lt;p&gt;Unlike the Z80, which swaps all main registers with EXX, Sampo is selective. Only registers R4-R11 (the arguments and temporaries) have shadow copies. The critical system registers (R0 (zero), R1 (return address), R2 (stack pointer), R3 (global pointer), and R12-R15 (saved registers)) are never swapped.&lt;/p&gt;
&lt;p&gt;This design decision serves interrupt handling. When an interrupt occurs, the handler can execute EXX to gain a fresh set of working registers without corrupting the interrupted code's arguments or temporaries. The stack pointer remains valid (no need to establish a new stack), and the return address register can be used to save the interrupted PC.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nl"&gt;irq_handler:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nf"&gt;EXX&lt;/span&gt;&lt;span class="w"&gt;                     &lt;/span&gt;&lt;span class="c1"&gt;; Swap to alternate R4-R11&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;; ... handle interrupt using R4'-R11' ...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;; Primary registers preserved automatically&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nf"&gt;EXX&lt;/span&gt;&lt;span class="w"&gt;                     &lt;/span&gt;&lt;span class="c1"&gt;; Swap back&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nf"&gt;RETI&lt;/span&gt;&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="c1"&gt;; Return from interrupt&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;The Flags Register&lt;/h3&gt;
&lt;p&gt;Sampo uses an 8-bit flags register with six defined flags:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bit&lt;/th&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Name&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;7&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;Negative&lt;/td&gt;
&lt;td&gt;Sign bit of result (bit 15)&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&lt;/td&gt;
&lt;td&gt;Result is zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;Carry&lt;/td&gt;
&lt;td&gt;Unsigned overflow / borrow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;V&lt;/td&gt;
&lt;td&gt;Overflow&lt;/td&gt;
&lt;td&gt;Signed overflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;H&lt;/td&gt;
&lt;td&gt;Half-carry&lt;/td&gt;
&lt;td&gt;Carry from bit 3 to 4 (for BCD)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;Interrupt&lt;/td&gt;
&lt;td&gt;Interrupt enable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The N, Z, C, and V flags follow standard conventions and support the full range of conditional branches. The H (half-carry) flag exists specifically for the DAA instruction, enabling correct BCD arithmetic. The I flag controls interrupt recognition.&lt;/p&gt;
&lt;p&gt;Notably, Sampo provides explicit GETF and SETF instructions to read and write the flags register, unlike many RISC architectures that treat flags as implicit state. This supports context switching and debugging.&lt;/p&gt;
&lt;h3&gt;Memory Model&lt;/h3&gt;
&lt;p&gt;Sampo uses a straightforward memory model:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Address space&lt;/strong&gt;: 64KB (16-bit addresses)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Byte-addressable&lt;/strong&gt;: Individual bytes can be loaded and stored&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Little-endian&lt;/strong&gt;: Multi-byte values stored with LSB at lower address&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Word alignment&lt;/strong&gt;: 16-bit words should be aligned on even addresses (optional enforcement)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A suggested memory map divides the 64KB space:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="n"&gt;x0000&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="n"&gt;x00FF&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="n"&gt;errupt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;vectors&lt;/span&gt;&lt;span class="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;Reset&lt;/span&gt;
&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="n"&gt;x0100&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="n"&gt;x7FFF&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;ROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;~&lt;/span&gt;&lt;span class="mf"&gt;32&lt;/span&gt;&lt;span class="n"&gt;KB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="n"&gt;x8000&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="n"&gt;xFEFF&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;RAM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;~&lt;/span&gt;&lt;span class="mf"&gt;32&lt;/span&gt;&lt;span class="n"&gt;KB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="n"&gt;xFF00&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="n"&gt;xFFFF&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;Memory&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mapped&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="p"&gt;(&lt;/span&gt;&lt;span class="mf"&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="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This layout provides a clean separation between code, data, and I/O while leaving room for customization. The interrupt vector area at the bottom of memory follows Z80 conventions, with the reset vector at 0x0000 and interrupt vector at 0x0004.&lt;/p&gt;
&lt;h4&gt;Port-Based I/O&lt;/h4&gt;
&lt;p&gt;In addition to memory, Sampo provides a separate 256-port I/O address space accessed via IN and OUT instructions. This design directly mirrors the Z80 and enables straightforward porting of code that interacts with serial ports, disk controllers, sound chips, and other peripherals.&lt;/p&gt;
&lt;p&gt;The I/O instructions come in two forms:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nf"&gt;INI&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;R4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0x80&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="c1"&gt;; Read from port 0x80 (immediate port number)&lt;/span&gt;
&lt;span class="nf"&gt;IN&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="no"&gt;R4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;R5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="c1"&gt;; Read from port specified in R5&lt;/span&gt;
&lt;span class="nf"&gt;OUTI&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0x81&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R4&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="c1"&gt;; Write R4 to port 0x81 (immediate)&lt;/span&gt;
&lt;span class="nf"&gt;OUT&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;R5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R4&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="c1"&gt;; Write R4 to port specified in R5&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Extended 32-bit forms (INX, OUTX) allow the full 8-bit port range to be specified in immediate form.&lt;/p&gt;
&lt;h3&gt;Instruction Encoding&lt;/h3&gt;
&lt;p&gt;Sampo uses a clean, regular encoding scheme with 16-bit base instructions and 32-bit extended forms. The 4-bit primary opcode in bits 15:12 determines the instruction category:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Opcode&lt;/th&gt;
&lt;th&gt;Category&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;0x0&lt;/td&gt;
&lt;td&gt;ADD&lt;/td&gt;
&lt;td&gt;Register addition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x1&lt;/td&gt;
&lt;td&gt;SUB&lt;/td&gt;
&lt;td&gt;Register subtraction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x2&lt;/td&gt;
&lt;td&gt;AND&lt;/td&gt;
&lt;td&gt;Bitwise AND&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x3&lt;/td&gt;
&lt;td&gt;OR&lt;/td&gt;
&lt;td&gt;Bitwise OR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x4&lt;/td&gt;
&lt;td&gt;XOR&lt;/td&gt;
&lt;td&gt;Bitwise XOR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x5&lt;/td&gt;
&lt;td&gt;ADDI&lt;/td&gt;
&lt;td&gt;Add immediate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x6&lt;/td&gt;
&lt;td&gt;LOAD&lt;/td&gt;
&lt;td&gt;Load from memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x7&lt;/td&gt;
&lt;td&gt;STORE&lt;/td&gt;
&lt;td&gt;Store to memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x8&lt;/td&gt;
&lt;td&gt;BRANCH&lt;/td&gt;
&lt;td&gt;Conditional branch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x9&lt;/td&gt;
&lt;td&gt;JUMP&lt;/td&gt;
&lt;td&gt;Unconditional jump/call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0xA&lt;/td&gt;
&lt;td&gt;SHIFT&lt;/td&gt;
&lt;td&gt;Shift and rotate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0xB&lt;/td&gt;
&lt;td&gt;MULDIV&lt;/td&gt;
&lt;td&gt;Multiply/divide/BCD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0xC&lt;/td&gt;
&lt;td&gt;MISC&lt;/td&gt;
&lt;td&gt;Stack, block ops, compare&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0xD&lt;/td&gt;
&lt;td&gt;I/O&lt;/td&gt;
&lt;td&gt;Port input/output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0xE&lt;/td&gt;
&lt;td&gt;SYSTEM&lt;/td&gt;
&lt;td&gt;NOP, HALT, interrupts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0xF&lt;/td&gt;
&lt;td&gt;EXTENDED&lt;/td&gt;
&lt;td&gt;32-bit instructions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;Instruction Formats&lt;/h4&gt;
&lt;p&gt;Six formats cover all instruction types:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Format R (Register-Register)&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c"&gt;15       12 11     8 7      4 3      0&lt;/span&gt;
&lt;span class="nb"&gt;+----------+--------+--------+--------+&lt;/span&gt;
&lt;span class="c"&gt;|  opcode  |   Rd   |  Rs1   |  Rs2   |&lt;/span&gt;
&lt;span class="nb"&gt;+----------+--------+--------+--------+&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Used for three-register operations like &lt;code&gt;ADD R4, R5, R6&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Format I (Immediate)&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c"&gt;15       12 11     8 7                0&lt;/span&gt;
&lt;span class="nb"&gt;+----------+--------+------------------+&lt;/span&gt;
&lt;span class="c"&gt;|  opcode  |   Rd   |      imm8        |&lt;/span&gt;
&lt;span class="nb"&gt;+----------+--------+------------------+&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Used for operations with 8-bit immediates like &lt;code&gt;ADDI R4, 42&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Format S (Store)&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c"&gt;15       12 11     8 7      4 3      0&lt;/span&gt;
&lt;span class="nb"&gt;+----------+--------+--------+--------+&lt;/span&gt;
&lt;span class="c"&gt;|  opcode  |  imm4  |  Rs1   |  Rs2   |&lt;/span&gt;
&lt;span class="nb"&gt;+----------+--------+--------+--------+&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Used for stores where the destination register field holds an offset.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Format B (Branch)&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c"&gt;15       12 11     8 7                0&lt;/span&gt;
&lt;span class="nb"&gt;+----------+--------+------------------+&lt;/span&gt;
&lt;span class="c"&gt;|  opcode  |  cond  |     offset8      |&lt;/span&gt;
&lt;span class="nb"&gt;+----------+--------+------------------+&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Used for conditional branches with PC-relative offsets.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Format J (Jump)&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c"&gt;15       12 11                       0&lt;/span&gt;
&lt;span class="nb"&gt;+----------+--------------------------+&lt;/span&gt;
&lt;span class="c"&gt;|  opcode  |        offset12          |&lt;/span&gt;
&lt;span class="nb"&gt;+----------+--------------------------+&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Used for unconditional jumps with 12-bit PC-relative offsets.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Format X (Extended)&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c"&gt;Word 0:&lt;/span&gt;
&lt;span class="c"&gt;15       12 11     8 7      4 3      0&lt;/span&gt;
&lt;span class="nb"&gt;+----------+--------+--------+--------+&lt;/span&gt;
&lt;span class="c"&gt;|   0xF    |   Rd   |  Rs1   |  sub   |&lt;/span&gt;
&lt;span class="nb"&gt;+----------+--------+--------+--------+&lt;/span&gt;

&lt;span class="c"&gt;Word 1:&lt;/span&gt;
&lt;span class="c"&gt;15                                   0&lt;/span&gt;
&lt;span class="nb"&gt;+-------------------------------------+&lt;/span&gt;
&lt;span class="c"&gt;|              imm16                  |&lt;/span&gt;
&lt;span class="nb"&gt;+-------------------------------------+&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Used for operations requiring 16-bit immediates or absolute addresses.&lt;/p&gt;
&lt;h4&gt;Encoding Examples&lt;/h4&gt;
&lt;p&gt;To illustrate the encoding scheme, let's examine several instructions:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ADD R4, R5, R6&lt;/strong&gt; (R4 = R5 + R6):&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;Opcode = 0x0, Rd = 4, Rs1 = 5, Rs2 = 6
Binary: 0000 0100 0101 0110 = 0x0456
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;ADDI R4, 10&lt;/strong&gt; (R4 = R4 + 10):&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;Opcode = 0x5, Rd = 4, imm8 = 10
Binary: 0101 0100 0000 1010 = 0x540A
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;BEQ +8&lt;/strong&gt; (branch forward 8 bytes if equal):&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;Opcode = 0x8, cond = 0 (BEQ), offset = 4 words
Binary: 1000 0000 0000 0100 = 0x8004
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;LIX R4, 0x1234&lt;/strong&gt; (load 16-bit immediate):&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;Word 0: 0xF (extended), Rd = 4, Rs = 0, sub = 7 (LIX)
Word 1: 0x1234
Binary: 1111 0100 0000 0111 0001 0010 0011 0100 = 0xF407 0x1234
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The regularity of this encoding makes instruction decode straightforward: the first nibble determines the instruction category, and subsequent fields are in consistent positions across formats.&lt;/p&gt;
&lt;h3&gt;The Instruction Set&lt;/h3&gt;
&lt;p&gt;Sampo provides approximately 66 distinct instructions, organized into ten categories.&lt;/p&gt;
&lt;h4&gt;Arithmetic (15 instructions)&lt;/h4&gt;
&lt;p&gt;The arithmetic category includes standard operations (ADD, SUB, ADDI) plus multiply/divide support:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MUL&lt;/strong&gt;: 16×16 multiplication, low 16 bits of result&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MULH/MULHU&lt;/strong&gt;: High 16 bits of 32-bit product (signed/unsigned)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DIV/DIVU&lt;/strong&gt;: Integer division (signed/unsigned)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;REM/REMU&lt;/strong&gt;: Remainder (signed/unsigned)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DAA&lt;/strong&gt;: Decimal adjust for BCD arithmetic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NEG&lt;/strong&gt;: Two's complement negation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CMP&lt;/strong&gt;: Compare (subtract without storing result)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hardware multiply and divide are essential for interpreter performance; dividing a 32-bit value by 10 for number formatting would be prohibitively slow without hardware support.&lt;/p&gt;
&lt;h4&gt;Logic (6 instructions)&lt;/h4&gt;
&lt;p&gt;Standard bitwise operations: AND, OR, XOR, NOT, plus immediate forms ANDI and ORI.&lt;/p&gt;
&lt;h4&gt;Shift and Rotate (16 variants)&lt;/h4&gt;
&lt;p&gt;Sampo provides an unusually rich set of shift operations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SLL/SRL/SRA&lt;/strong&gt;: Shift left/right logical/arithmetic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ROL/ROR&lt;/strong&gt;: Rotate left/right&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RCL/RCR&lt;/strong&gt;: Rotate through carry (17-bit rotation)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SWAP&lt;/strong&gt;: Swap high and low bytes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each shift type comes in three shift amounts: 1, 4, and 8 bits. The 4-bit shift is particularly useful for hexadecimal digit extraction and insertion. Variable shifts use the extended format with the shift amount in the second register or immediate field.&lt;/p&gt;
&lt;h4&gt;Load/Store (6 instructions)&lt;/h4&gt;
&lt;p&gt;Memory access instructions include word and byte loads (with sign or zero extension), word and byte stores, and LUI (Load Upper Immediate) for constructing 16-bit constants:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nf"&gt;LUI&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;R4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0x12&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="c1"&gt;; R4 = 0x1200&lt;/span&gt;
&lt;span class="nf"&gt;ORI&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;R4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0x34&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;; R4 = 0x1234&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Branch (16 conditions)&lt;/h4&gt;
&lt;p&gt;Sampo supports a comprehensive set of branch conditions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;BEQ/BNE&lt;/strong&gt;: Equal/not equal&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BLT/BGE/BGT/BLE&lt;/strong&gt;: Signed comparisons&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BLTU/BGEU/BHI/BLS&lt;/strong&gt;: Unsigned comparisons&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BMI/BPL&lt;/strong&gt;: Negative/positive&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BVS/BVC&lt;/strong&gt;: Overflow set/clear&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BCS/BCC&lt;/strong&gt;: Carry set/clear&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This covers all reasonable comparison outcomes for both signed and unsigned arithmetic.&lt;/p&gt;
&lt;h4&gt;Jump/Call (4 instructions)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;J&lt;/strong&gt;: PC-relative unconditional jump&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JAL&lt;/strong&gt;: Jump and link (save return address in RA)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JR&lt;/strong&gt;: Jump to address in register&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JALR&lt;/strong&gt;: Jump and link to register address&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Block Operations (6 instructions)&lt;/h4&gt;
&lt;p&gt;The block operations use a fixed register convention (R4=count, R5=source, R6=destination):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LDI/LDD&lt;/strong&gt;: Load single byte, increment/decrement pointers and count&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LDIR/LDDR&lt;/strong&gt;: Repeat until count reaches zero&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FILL&lt;/strong&gt;: Fill memory region with value&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPIR&lt;/strong&gt;: Compare and search forward&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These instructions are decidedly un-RISC; they're multi-cycle operations that modify multiple registers. But they're implemented with predictable behavior (always the same registers, always the same algorithm) and provide enormous speedups for common memory operations.&lt;/p&gt;
&lt;h4&gt;Stack (4 instructions)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PUSH/POP&lt;/strong&gt;: Single register push/pop&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PUSHM/POPM&lt;/strong&gt;: Push/pop multiple registers (via bitmask)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;I/O (4 instructions)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;INI/OUTI&lt;/strong&gt;: Immediate port address&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IN/OUT&lt;/strong&gt;: Register port address&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;System (9 instructions)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;NOP&lt;/strong&gt;: No operation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HALT&lt;/strong&gt;: Stop processor&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DI/EI&lt;/strong&gt;: Disable/enable interrupts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;EXX&lt;/strong&gt;: Exchange alternate registers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RETI&lt;/strong&gt;: Return from interrupt&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SWI&lt;/strong&gt;: Software interrupt&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SCF/CCF&lt;/strong&gt;: Set/complement carry flag&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GETF/SETF&lt;/strong&gt;: Read/write flags register&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Comparison with Other Architectures&lt;/h3&gt;
&lt;p&gt;To put Sampo in context, consider how it compares with related processors:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Z80&lt;/th&gt;
&lt;th&gt;MIPS&lt;/th&gt;
&lt;th&gt;RISC-V&lt;/th&gt;
&lt;th&gt;Sampo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Word size&lt;/td&gt;
&lt;td&gt;8-bit&lt;/td&gt;
&lt;td&gt;32-bit&lt;/td&gt;
&lt;td&gt;32/64-bit&lt;/td&gt;
&lt;td&gt;16-bit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instruction width&lt;/td&gt;
&lt;td&gt;1-4 bytes&lt;/td&gt;
&lt;td&gt;4 bytes&lt;/td&gt;
&lt;td&gt;2/4 bytes&lt;/td&gt;
&lt;td&gt;2/4 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Registers&lt;/td&gt;
&lt;td&gt;8 + alternates&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;16 + alternates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zero register&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;$zero&lt;/td&gt;
&lt;td&gt;x0&lt;/td&gt;
&lt;td&gt;R0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;I/O model&lt;/td&gt;
&lt;td&gt;Port-based&lt;/td&gt;
&lt;td&gt;Memory-mapped&lt;/td&gt;
&lt;td&gt;Memory-mapped&lt;/td&gt;
&lt;td&gt;Port-based&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Block operations&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instruction count&lt;/td&gt;
&lt;td&gt;~300+&lt;/td&gt;
&lt;td&gt;~60&lt;/td&gt;
&lt;td&gt;~50 base&lt;/td&gt;
&lt;td&gt;~66&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Sampo sits in an interesting position: more regular than the Z80 but with Z80-friendly features, smaller and simpler than 32-bit RISC but still cleanly orthogonal.&lt;/p&gt;
&lt;h3&gt;Code Examples&lt;/h3&gt;
&lt;p&gt;To demonstrate how Sampo assembly looks in practice, here's a "Hello World" program that outputs text via a serial port:&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="na"&gt;.org&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0x0100&lt;/span&gt;

&lt;span class="na"&gt;.equ&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="no"&gt;ACIA_STATUS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0x80&lt;/span&gt;
&lt;span class="na"&gt;.equ&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="no"&gt;ACIA_DATA&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;0x81&lt;/span&gt;
&lt;span class="na"&gt;.equ&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="no"&gt;TX_READY&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="mi"&gt;0x02&lt;/span&gt;

&lt;span class="nl"&gt;start:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nf"&gt;LIX&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;R4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;message&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;; Load address of string&lt;/span&gt;

&lt;span class="nl"&gt;loop:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nf"&gt;LBU&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;R5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;R4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="c1"&gt;; Load byte from string&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nf"&gt;CMP&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;R5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R0&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="c1"&gt;; Compare with zero&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nf"&gt;BEQ&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;done&lt;/span&gt;&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="c1"&gt;; If null terminator, done&lt;/span&gt;

&lt;span class="nl"&gt;wait_tx:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nf"&gt;INI&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;R6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;ACIA_STATUS&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;; Read serial status port&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nf"&gt;ANDI&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;TX_READY&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;; Check transmit ready bit&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nf"&gt;BEQ&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;wait_tx&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="c1"&gt;; Wait if not ready&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nf"&gt;OUTI&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;ACIA_DATA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R5&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c1"&gt;; Write character to data port&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nf"&gt;ADDI&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R4&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="c1"&gt;; Next character&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nf"&gt;J&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="no"&gt;loop&lt;/span&gt;
&lt;span class="nl"&gt;done:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nf"&gt;HALT&lt;/span&gt;

&lt;span class="nl"&gt;message:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="na"&gt;.asciz&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Hello, Sampo!\n"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And here's a Fibonacci function demonstrating the calling convention:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;; fib(n) - compute nth Fibonacci number&lt;/span&gt;
&lt;span class="c1"&gt;; Input: R4 (A0) = n&lt;/span&gt;
&lt;span class="c1"&gt;; Output: R4 (A0) = fib(n)&lt;/span&gt;

&lt;span class="nl"&gt;fib:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nf"&gt;ADDI&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R0&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="c1"&gt;; a = 0&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nf"&gt;ADDI&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R0&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="c1"&gt;; b = 1&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nf"&gt;CMP&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;R4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R0&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nf"&gt;BEQ&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;fib_done&lt;/span&gt;

&lt;span class="nl"&gt;fib_loop:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nf"&gt;ADD&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;R7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R6&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="c1"&gt;; temp = a + b&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nf"&gt;ADD&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;R5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R0&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="c1"&gt;; a = b&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nf"&gt;ADD&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;R6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R0&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="c1"&gt;; b = temp&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nf"&gt;ADDI&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R4&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="c1"&gt;; n--&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nf"&gt;BNE&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;fib_loop&lt;/span&gt;

&lt;span class="nl"&gt;fib_done:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nf"&gt;ADD&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="no"&gt;R4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;R0&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="c1"&gt;; return a&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nf"&gt;JR&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="no"&gt;RA&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The code reads naturally to anyone familiar with RISC assembly, while the I/O instructions and register conventions provide the Z80-like feel that makes porting classic software straightforward.&lt;/p&gt;
&lt;h3&gt;Looking Ahead: FPGA Implementation&lt;/h3&gt;
&lt;p&gt;With the architecture defined, the next step is implementation. In Part 2 of this series, we'll build a working Sampo processor using Amaranth HDL, a modern Python-based hardware description language. We'll cover:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The ALU module&lt;/strong&gt;: Implementing all arithmetic and logic operations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The register file&lt;/strong&gt;: Including the alternate register set and zero register&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The instruction decoder&lt;/strong&gt;: Parsing the various instruction formats&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The control unit&lt;/strong&gt;: Managing the fetch-decode-execute cycle&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The memory interface&lt;/strong&gt;: Connecting to block RAM&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The I/O subsystem&lt;/strong&gt;: Implementing the port-based I/O model&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integration&lt;/strong&gt;: Putting it all together into a working system-on-chip&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We'll synthesize the design for an affordable &lt;a href="https://baud.rs/OE4qHU"&gt;FPGA board&lt;/a&gt; and run actual Sampo programs, demonstrating that this architecture isn't just a paper exercise but a real, working processor.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://baud.rs/r74wA8"&gt;Sampo project on GitHub&lt;/a&gt; includes a complete &lt;a href="https://baud.rs/gSnSwR"&gt;Rust-based&lt;/a&gt; assembler (sasm) and emulator (semu) with a TUI debugger, so you can start writing and testing Sampo programs today. The FPGA implementation will let you run those same programs on real hardware, completing the journey from mythological artifact to silicon reality.&lt;/p&gt;
&lt;p&gt;Stay tuned for Part 2, where we'll forge our own Sampo, not from swan feathers and barley, but from lookup tables and flip-flops.&lt;/p&gt;</description><category>computer architecture</category><category>cpu design</category><category>retrocomputing</category><category>risc</category><category>sampo</category><category>z80</category><guid>https://tinycomputers.io/posts/sampo-16-bit-risc-cpu-part-1.html</guid><pubDate>Tue, 30 Dec 2025 15:00:00 GMT</pubDate></item></channel></rss>