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

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

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

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

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

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

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

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

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

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

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

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