<?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 pcb design)</title><link>https://tinycomputers.io/</link><description></description><atom:link href="https://tinycomputers.io/categories/pcb-design.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>Fri, 24 Apr 2026 17:16:02 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>What a Commercial PCB Placer Does That My Open-Source One Can't</title><link>https://tinycomputers.io/posts/what-a-commercial-pcb-placer-does-that-my-open-source-one-cant.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/what-a-commercial-pcb-placer-does-that-my-open-source-one-cant_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;28 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;div class="sponsor-widget"&gt;
&lt;div class="sponsor-widget-header"&gt;&lt;a href="https://baud.rs/wdr0dP"&gt;Quilter&lt;/a&gt; · Partner&lt;/div&gt;
&lt;p&gt;I partnered with &lt;a href="https://baud.rs/wdr0dP"&gt;Quilter.ai&lt;/a&gt; on this post. They provided access to their commercial placement service at no cost so I could run this comparison. The open-source placer I built to benchmark against them, the evaluation methodology, and every number in this post are mine.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;This is an interlude in a running series on designing a level-shifter shield for the &lt;a href="https://baud.rs/poSQeo"&gt;Arduino Giga R1&lt;/a&gt;. The main arc is about the design itself — how to get 72 channels of logic-level translation across a 155mm x 90mm board using ten SN74LVC8T245 shifters, in a repeatable Python-scripted workflow. Previous posts covered the &lt;a href="https://tinycomputers.io/posts/fiverr-pcb-design-arduino-giga-shield.html"&gt;original Fiverr design&lt;/a&gt;, the &lt;a href="https://tinycomputers.io/posts/redesigning-a-pcb-with-claude-code-and-open-source-eda-part-1.html"&gt;Claude Code redesign&lt;/a&gt;, &lt;a href="https://tinycomputers.io/posts/how-a-pin-numbering-bug-killed-a-pcb.html"&gt;a pin-numbering bug&lt;/a&gt;, and the &lt;a href="https://tinycomputers.io/posts/what-routing-314-nets-taught-me-about-ai-assisted-pcb-design.html"&gt;v0.4 respin&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This post is an honest comparison of two ways to place the components on that board: Quilter, a commercial service aimed at exactly this problem, and pyplacer, an open-source simulated-annealing placer I wrote over two weekends specifically to have something to benchmark Quilter against. The results are not flattering to pyplacer, but they are interesting. They also surface a concrete and narrow thing that commercial placement services do right that a naive first pass does not.&lt;/p&gt;
&lt;h3&gt;Why Write a Placer At All&lt;/h3&gt;
&lt;p&gt;A PCB has two distinct layout problems. The first is &lt;em&gt;placement&lt;/em&gt;: deciding where each component goes on the board. The second is &lt;em&gt;routing&lt;/em&gt;: deciding how to connect the pins once the components are placed. These are usually solved by different tools, and the quality of the placement determines how well the routing can go. A bad placement makes a good routing impossible; a good placement makes routing almost boring.&lt;/p&gt;
&lt;p&gt;There is no good open-source placement tool. Freerouting, which is genuinely excellent as an autorouter, does not do placement — it takes placed components as input. KiCad has a rudimentary "move to group" feature and nothing else. Eagle has nothing. pcb-rnd has nothing. For decades, PCB placement has been a human-only activity, and the industry's answer to "automate placement" has been to employ better humans.&lt;/p&gt;
&lt;p&gt;A few commercial services are now genuinely trying to change that. &lt;a href="https://baud.rs/wdr0dP"&gt;Quilter&lt;/a&gt; is one of them: you upload a KiCad board with component libraries and a netlist, it returns a placed-and-routed board. I approached Quilter about partnering on a post and I decided that a straight review would be uninteresting. What I actually wanted to know was: how much of the gap between Quilter and "do nothing" could be closed by a motivated hobbyist with a weekend, some Python, and a simulated annealer? If the gap is small, Quilter is an incremental improvement. If the gap is large, Quilter is doing something specific that is hard to reproduce.&lt;/p&gt;
&lt;p&gt;So I built &lt;a href="https://baud.rs/tcrfa0"&gt;pyplacer&lt;/a&gt;. The goal was not to beat Quilter — that would have been foolish. The goal was to produce a credible open-source baseline, run both tools on the same board under the same constraints, and see how the numbers fell.&lt;/p&gt;
&lt;h3&gt;The Board&lt;/h3&gt;
&lt;p&gt;The benchmark is &lt;a href="https://baud.rs/pOawfA"&gt;giga_shield v0.4&lt;/a&gt; — the same board discussed in the previous post. Relevant specs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;155mm x 90mm, 4-layer stack (two signal, two plane)&lt;/li&gt;
&lt;li&gt;161 nets across 64 components&lt;/li&gt;
&lt;li&gt;Ten SN74LVC8T245PW level-shifter ICs in TSSOP-24 packages&lt;/li&gt;
&lt;li&gt;Two 36-pin 2x18 headers (J9 and J10) along the right edge for the Z80 side&lt;/li&gt;
&lt;li&gt;Several 8-pin and 26-pin single-row headers for the Arduino Giga side&lt;/li&gt;
&lt;li&gt;Forty 0603 bypass and DIR-control caps/resistors&lt;/li&gt;
&lt;li&gt;All components on the top side of the board&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is a &lt;em&gt;dense&lt;/em&gt; layout for 161 nets on 155x90mm. The level shifters are the routing chokepoint: each one has 8 A-side pins that connect to one header and 8 B-side pins that connect to another, plus VCCA/VCCB/GND/DIR. Get the shifters wrong relative to the headers and the board is unroutable at 4 layers.&lt;/p&gt;
&lt;p&gt;The honest baseline for comparison is not "random placement." It is &lt;em&gt;my&lt;/em&gt; hand-placed v0.4 layout, which Freerouting routes to 100% at 0.3mm clearance in about 45 minutes of wall-clock time. I know that because I spent several weeks iterating to produce it. The goal of any automated placer is to do that work so I do not have to.&lt;/p&gt;
&lt;h3&gt;Quilter: The Commercial Option&lt;/h3&gt;
&lt;p&gt;Quilter presents as a web service: you upload your &lt;code&gt;.kicad_pcb&lt;/code&gt; (unplaced) with its component libraries and netlist, set a few parameters (board outline, fixed components, target layer count), and they generate candidate placements.&lt;/p&gt;
&lt;p&gt;For this comparison I submitted giga_shield v0.4 with the ten shifters and all passives unplaced, the four headers and mounting holes locked in position (because those are mechanical constraints tied to the Arduino Giga's hardware), and the board outline fixed. Quilter returned &lt;code&gt;Candidate_1&lt;/code&gt; after 1 hour 10 minutes 48 seconds of compute.&lt;/p&gt;
&lt;div style="text-align: center; margin: 30px 0;"&gt;
&lt;img src="https://tinycomputers.io/images/giga-shield/quilter-top.png" alt="Top-down render of Quilter's Candidate_1 placement. Ten level shifters distributed across the board: three in a row along the top edge near J5, two in the bottom third near J8, and five clustered between J9 and J10 on the right-hand side. Passives are scattered across the routing channels in short local clusters. The board has routing traces visible on the top and inner layers." style="max-width: 100%; border: 1px solid #ddd; border-radius: 8px;"&gt;
&lt;p style="color: #666; font-size: 12px; margin-top: 10px;"&gt;Quilter Candidate_1, rendered via &lt;code&gt;kicad-cli pcb render&lt;/code&gt;. Ten shifters spread into three clusters, each one closest to the connectors it serves. Routing threads through every available channel.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;Freerouting, given Quilter's placement and the same 0.3mm clearance rule I used for my hand-placement, routed 99.4% of the board: 1 net unrouted out of 161. The single unrouted net was a non-critical DIR control signal that could have been fixed with a manual jumper or a small placement tweak. For practical purposes, the board is fabricatable as Quilter produced it.&lt;/p&gt;
&lt;p&gt;That is a strong result. It is in the same quality neighborhood as my hand-placement, produced in about seventy minutes of their wall-clock time versus my several weeks of iteration.&lt;/p&gt;
&lt;h3&gt;pyplacer: The Open-Source Attempt&lt;/h3&gt;
&lt;p&gt;pyplacer is about 2,000 lines of Python. It does &lt;a href="https://baud.rs/rZysbW"&gt;simulated annealing&lt;/a&gt; over component positions on a fixed board outline, with a cost function that combines half-perimeter wirelength, bounding-box overlap penalties, an out-of-bounds penalty, a grid-cell congestion estimate from probing L-shaped routes across a coarse mesh, and a small pad-exit direction bias that tries to align TSSOP-24 pads with their natural routing sides.&lt;/p&gt;
&lt;p&gt;The overall architecture is pragmatic rather than clever:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A KiCad board loader that reads footprints, pads, and netlist&lt;/li&gt;
&lt;li&gt;A heuristic seed placement that puts each shifter at the midpoint between its two dominant connectors (the connectors that account for the most of its signal pins), with special handling for clusters of shifters sharing the same connector pair&lt;/li&gt;
&lt;li&gt;An SA loop: propose a move (shift, swap, or component-to-component swap), evaluate the cost delta, accept with &lt;a href="https://baud.rs/09b03A"&gt;Metropolis probability&lt;/a&gt; at the current temperature, cool&lt;/li&gt;
&lt;li&gt;A DSN exporter that writes a Specctra file with the placed positions and the netlist, ready to feed to Freerouting&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The cost function I arrived at after maybe a dozen tuning rounds:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;W_HPWL&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;baseline&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;wirelength&lt;/span&gt;
&lt;span class="n"&gt;W_OVERLAP_HARD&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1000.0&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;two&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;may&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;overlap&lt;/span&gt;
&lt;span class="n"&gt;W_OVERLAP_SOFT&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;50.0&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keepout&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;zones&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;around&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;components&lt;/span&gt;
&lt;span class="n"&gt;W_OUT_OF_BOUNDS&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;200.0&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stay&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;
&lt;span class="n"&gt;W_CONGESTION&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;40.0&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;probe&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;based&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;congestion&lt;/span&gt;
&lt;span class="n"&gt;W_PAD_EXIT&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bias&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pads&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;toward&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;natural&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;routing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sides&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Everything else — rotation moves, attachment-to-dominant-connector, displacement-from-heuristic — I tried and disabled, because each one either produced worse results or did not produce a measurable improvement.&lt;/p&gt;
&lt;p&gt;The SA run itself is 5,000 iterations at 0.96 cooling with a 1e-4 final-temperature ratio, taking about 90 seconds on a single core of a Mac M3 Pro. That is fast enough that I could do broad seed sweeps cheaply.&lt;/p&gt;
&lt;p&gt;For the benchmark, I ran pyplacer on 64 distinct random seeds across three machines (my Mac, a 32-core Linux host, a 64-core Linux host), routed every resulting placement through Freerouting at the same 0.3mm clearance, and recorded the number of unrouted nets. The seed distribution matters because SA is stochastic: different seeds give different local minima, and the spread of outcomes is informative.&lt;/p&gt;
&lt;h3&gt;The Numbers&lt;/h3&gt;
&lt;p&gt;Over 64 seeds:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Best result: 10 unrouted nets (93.8% routed) — this was the early best from the first batch of runs&lt;/li&gt;
&lt;li&gt;Typical plateau result: 16–20 unrouted nets (87.6%–90.1% routed)&lt;/li&gt;
&lt;li&gt;Worst result: 114 unrouted nets (29.2% routed)&lt;/li&gt;
&lt;li&gt;Median: 21 unrouted nets (87.0% routed)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every single pyplacer run was worse than Quilter's &lt;code&gt;Candidate_1&lt;/code&gt;. The gap between the best pyplacer seed and Quilter is about six percentage points of routing completion, which on a 161-net board is nine to fifteen missing traces. In practice, a board with 10–16 unrouted nets is not fabricatable as-is. It needs hand-fixing, which is exactly the work the placer was supposed to save.&lt;/p&gt;
&lt;div style="text-align: center; margin: 30px 0;"&gt;
&lt;img src="https://tinycomputers.io/images/giga-shield/pyplacer-top-routed.png" alt="Top-down render of a pyplacer placement after Freerouting. All ten level shifters clustered tightly in a horizontal band across the middle of the board, immediately to the left of J9. Routing traces visible, but the shifter cluster is visibly cramped and many nets run long distances to reach it." style="max-width: 100%; border: 1px solid #ddd; border-radius: 8px;"&gt;
&lt;p style="color: #666; font-size: 12px; margin-top: 10px;"&gt;A pyplacer placement after a Freerouting pass. Notice how the shifters are crammed into a single cluster hugging the left edge of J9, and the routing channel on that edge is visibly dense. This layout routes to about 90%; the unrouted nets are all in the congested zone on the left side of J9.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;Compare that image to the Quilter render above. The topological difference is immediate: Quilter spreads its shifters into several clusters, each close to the connector or connector-pair it serves. pyplacer piles them all into the shortest-wirelength position, which happens to be a narrow strip where routes cannot fan out. The visual story matches the numbers.&lt;/p&gt;
&lt;h3&gt;Which Nets Failed&lt;/h3&gt;
&lt;p&gt;I logged Freerouting's warning-level output across the entire 64-seed sweep and extracted the nets that consistently failed to route on the best seed's plateau. Seventeen candidate nets, of which sixteen appear in any given plateau pass. The surprising thing is not the count — it is the topology:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Net&lt;/th&gt;
&lt;th&gt;Pins&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;D23&lt;/td&gt;
&lt;td&gt;J9-4 ↔ U8-3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D25&lt;/td&gt;
&lt;td&gt;J9-6 ↔ U8-4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D27&lt;/td&gt;
&lt;td&gt;J9-8 ↔ U8-5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D29&lt;/td&gt;
&lt;td&gt;J9-10 ↔ U8-7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D31&lt;/td&gt;
&lt;td&gt;J9-12 ↔ U8-8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D33&lt;/td&gt;
&lt;td&gt;J9-14 ↔ U8-9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D35&lt;/td&gt;
&lt;td&gt;J9-16 ↔ U8-10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D37&lt;/td&gt;
&lt;td&gt;J9-18 ↔ U9-2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D39&lt;/td&gt;
&lt;td&gt;J9-20 ↔ U9-3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D41&lt;/td&gt;
&lt;td&gt;J9-22 ↔ U9-5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D43&lt;/td&gt;
&lt;td&gt;J9-24 ↔ U7-3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D45&lt;/td&gt;
&lt;td&gt;J9-26 ↔ U7-5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D47&lt;/td&gt;
&lt;td&gt;J9-28 ↔ U7-8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D49&lt;/td&gt;
&lt;td&gt;J9-30 ↔ U7-10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D51&lt;/td&gt;
&lt;td&gt;J9-32 ↔ U10-4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PJ5&lt;/td&gt;
&lt;td&gt;J10-16 ↔ U8-13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PB1&lt;/td&gt;
&lt;td&gt;J1-4  ↔ U2-5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Fifteen of the seventeen are the exact same topological class: a short digital signal from a level shifter to a sequential pin on the J9 double-row header. These are the easiest routes on the board. They fail because pyplacer has piled all four shifters (U7, U8, U9, U10) up against J9's left edge, and sixteen signals are trying to enter J9 through a 10mm-wide routing channel that can physically only accommodate maybe eight parallel traces.&lt;/p&gt;
&lt;p&gt;This is a connector-edge congestion failure. It is not a routing problem. It is a placement problem that the router cannot undo.&lt;/p&gt;
&lt;h3&gt;Why pyplacer Gets This Wrong&lt;/h3&gt;
&lt;p&gt;Half-perimeter wirelength rewards placing U7/U8/U9/U10 as close to J9 as possible, because every one of their B-side pins connects to J9 and the distance matters. The bounding-box penalty keeps them from overlapping each other. The congestion estimate &lt;em&gt;does&lt;/em&gt; catch some of this — it is why pyplacer's placements are not as awful as they would be without it — but the resolution of the congestion grid is too coarse to catch what is happening at J9's edge specifically. The cost function sees "OK, the shifters are stacked but they are not literally on top of each other, and the average 2x2mm cell is not saturated." The router sees "sixteen traces trying to enter the same five-millimeter gap, no thanks."&lt;/p&gt;
&lt;p&gt;Fixing this correctly is not just a matter of tuning weights. The cost function is missing a dimension. Specifically, it needs a term that penalizes &lt;em&gt;incoming pin density at a fixed connector's edge&lt;/em&gt; — a count of how many signal nets want to enter a given side of each fixed header per millimeter of edge, with a soft threshold above which the penalty climbs sharply. A naive implementation of this adds about 50 lines to the cost function and a per-move incremental update. I have not yet written it because the interesting finding of this benchmark is not "how do I close the gap" — that is the next post. The interesting finding is &lt;em&gt;where&lt;/em&gt; the gap lives.&lt;/p&gt;
&lt;p&gt;Quilter, whatever it is doing internally, is not piling shifters up against J9. It spreads them. U7, U8, U9, and U10 in Quilter's placement are in a 4x1 tight column arrangement a few millimeters away from J9's edge, with gaps between each chip that are deliberately large enough for routes to fan out between them. That does not minimize wirelength. It maximizes &lt;em&gt;routability&lt;/em&gt;. The two are not the same objective, and pyplacer only optimizes the first.&lt;/p&gt;
&lt;h3&gt;Speed and Cost&lt;/h3&gt;
&lt;p&gt;A fair comparison also needs to account for what each approach costs to produce.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Quilter&lt;/strong&gt;: &lt;code&gt;Candidate_1&lt;/code&gt; took 1h 10m 48s of their wall-clock compute. Pricing is on their website; I received this candidate through my partnership with Quilter and did not personally pay. One good candidate is enough for a finished board, so the total spend for a project like this is whatever a single candidate costs, not a subscription or a retainer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;pyplacer&lt;/strong&gt;: A single seed takes about 90 seconds of single-core wall-clock time. A 64-seed sweep with partial parallelism across three machines took about 35 minutes of real time and consumed roughly three CPU-hours. The Freerouting pass that evaluates each placement takes 10–15 minutes per candidate, so evaluating all 64 placements took an additional ~15 CPU-hours, mostly on Linux hosts. Electricity cost is negligible — pennies. But the human-hours cost of writing, tuning, and debugging pyplacer was maybe 25 hours of my weekend time spread across two weeks.&lt;/p&gt;
&lt;p&gt;So: Quilter produces a usable placement in about seventy minutes of their compute, at whatever their per-candidate list price is. pyplacer cost 25 hours of engineering time, some CPU, and still does not produce a usable placement. The break-even point, assuming my engineering time is valued at even a modest hourly rate, favors Quilter by a wide margin for single-project work. The calculus only flips if you are going to place hundreds of boards and amortize the pyplacer development cost across all of them. For a hobby project or a one-off prototype, it is not close.&lt;/p&gt;
&lt;h3&gt;What About Future Improvements?&lt;/h3&gt;
&lt;p&gt;The obvious counter is that pyplacer is a weekend project and Quilter has been in development for years with a team. If I added the connector-edge congestion term, fixed the coarse grid resolution, added rotation moves, and let the SA run for 10x longer, would I close the gap?&lt;/p&gt;
&lt;p&gt;Probably partially. Maybe to 95–97%. I do not think I would close it fully. Here is why.&lt;/p&gt;
&lt;p&gt;Quilter is doing things that are not obvious from its output but are visible in the spacing patterns: it seems to have a model of the router's behavior, of how close is "too close" for vias to cluster, of how routing channels compose across layers. A simulated annealer can be taught any cost function you can express, but the cost function has to know to include these things. Writing a cost function that captures what Quilter captures is not a weekend project. It is months of work by someone who understands routing at a deep level, which I do not.&lt;/p&gt;
&lt;p&gt;I could also throw more compute. The 64-seed sweep took ~18 CPU-hours total. If I ran 1,000 seeds on a cluster, I would likely find a placement better than the best seed from 64, by selection pressure alone. But the ceiling set by the cost function's limitations is not breached by more samples; it is breached by a better objective function. And that objective function is where the commercial tool is earning its fee.&lt;/p&gt;
&lt;h3&gt;What This Means for the Series&lt;/h3&gt;
&lt;p&gt;The immediate practical takeaway for the GigaShield series is that if I wanted to save time on future revisions of this board, running it through Quilter and accepting &lt;code&gt;Candidate_1&lt;/code&gt; would be a sensible default — especially for the v1.0 redesign where I want to add thermal pads, improve the layer stack, and shuffle the power rails. That is a genuine upgrade over the hand-placement workflow, which, while competent, took me multiple weeks of iteration to get right.&lt;/p&gt;
&lt;p&gt;The broader takeaway is that placement is a harder problem than routing. Freerouting is an excellent autorouter because routing has a clean, well-posed objective: find paths that respect clearance rules and minimize length. Placement has no such clean objective. A "good" placement is one the router can finish, which is not something you can compute directly from the placement — you only find out by running the router. That feedback loop makes placement the sort of problem where stochastic search without a good model does not close the gap with humans or with informed commercial tools.&lt;/p&gt;
&lt;p&gt;I am going to keep iterating on pyplacer because I find the problem interesting and because an open-source baseline has value even when it is worse than the commercial alternative. The connector-edge congestion term is the first thing I will add. After that, I want to look at rotation moves in the SA, because the ability to flip a shifter's A/B-side orientation is exactly the sort of thing that can unclog a congested connector edge at the cost of a small HPWL penalty.&lt;/p&gt;
&lt;p&gt;But I am not going to pretend I am closing the gap to Quilter through cleverness alone. Whatever Quilter is doing, it is working. For a dense board with real constraints and a real fabrication deadline, their output is better than mine.&lt;/p&gt;
&lt;h3&gt;The Honest Review&lt;/h3&gt;
&lt;p&gt;Companies that partner with me on posts sometimes expect the post to soften the criticism of their tool. Quilter, to their credit, did not. I uploaded my own board, they ran it through their service, and they let me analyze the output however I wanted. The result is that this is a post about an open-source placer that loses to a commercial one, not a post about a commercial tool defeating a straw man.&lt;/p&gt;
&lt;p&gt;For the specific use case I built this around — dense four-layer mixed-signal shields with 150+ nets and tight routing channels — Quilter's &lt;code&gt;Candidate_1&lt;/code&gt; is better than what I can produce with a weekend placer. The gap is not close. The gap is specifically at connector-edge congestion, which is a place where my naive objective function fails and theirs does not. That is a real and informative finding, and the right conclusion is that whatever Quilter charges per candidate is a reasonable price for what you get.&lt;/p&gt;
&lt;p&gt;I would recommend them for boards like this one. I would not recommend them for dead-simple boards where a hand placement in KiCad takes twenty minutes — that is still the cheapest path for trivial work. The interesting threshold is somewhere in the range of 50–100 nets and a non-trivial connector layout; below that, do it yourself; above that, pay the pros.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Previous posts in the GigaShield series: &lt;a href="https://tinycomputers.io/posts/what-routing-314-nets-taught-me-about-ai-assisted-pcb-design.html"&gt;What Routing 314 Nets Taught Me About AI-Assisted PCB Design&lt;/a&gt; · &lt;a href="https://tinycomputers.io/posts/how-a-pin-numbering-bug-killed-a-pcb.html"&gt;How a Pin Numbering Bug Killed a PCB&lt;/a&gt; · &lt;a href="https://tinycomputers.io/posts/redesigning-a-pcb-with-claude-code-and-open-source-eda-part-1.html"&gt;Redesigning with Claude Code (Part 1)&lt;/a&gt; · &lt;a href="https://tinycomputers.io/posts/fiverr-pcb-design-arduino-giga-shield.html"&gt;Fiverr PCB Design (\$468)&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</description><category>arduino giga</category><category>benchmark</category><category>freerouting</category><category>level shifter</category><category>open-source</category><category>pcb design</category><category>placement</category><category>pyplacer</category><category>quilter</category><category>retroshield</category><category>simulated annealing</category><category>z80</category><guid>https://tinycomputers.io/posts/what-a-commercial-pcb-placer-does-that-my-open-source-one-cant.html</guid><pubDate>Fri, 24 Apr 2026 15:30:00 GMT</pubDate></item><item><title>What Routing 314 Nets Taught Me About AI-Assisted PCB Design</title><link>https://tinycomputers.io/posts/what-routing-314-nets-taught-me-about-ai-assisted-pcb-design.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/what-routing-314-nets-taught-me-about-ai-assisted-pcb-design_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;div class="sponsor-widget"&gt;
&lt;div class="sponsor-widget-header"&gt;&lt;a href="https://baud.rs/youwpy"&gt;&lt;img src="https://tinycomputers.io/images/pcbway-logo.png" alt="PCBWay" style="height: 22px; vertical-align: middle; margin-right: 8px;"&gt;&lt;/a&gt; Sponsored Hardware&lt;/div&gt;
&lt;p&gt;The GigaShield boards discussed in this post were fabricated by &lt;a href="https://baud.rs/youwpy"&gt;PCBWay&lt;/a&gt;. Their DFM review caught the clearance issue that forms most of this story's middle act — which, as it turns out, is exactly the right kind of thing to catch before the copper is cut. PCBWay offers PCB prototyping, assembly, CNC machining, and 3D printing services with turnaround times starting at 24 hours. &lt;a href="https://baud.rs/youwpy"&gt;pcbway.com&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;This is the fourth post in a running series about designing a level-shifter shield for the &lt;a href="https://baud.rs/poSQeo"&gt;Arduino Giga R1&lt;/a&gt; using &lt;a href="https://baud.rs/Z6Oq4k"&gt;Claude Code&lt;/a&gt; and open-source command-line EDA tools. A brief map of what's come before, since the series grew past its original planned scope:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tinycomputers.io/posts/fiverr-pcb-design-arduino-giga-shield.html"&gt;Fiverr PCB Design (\$468)&lt;/a&gt; — the first GigaShield, designed by a freelance contractor in KiCad. It worked for most things but broke against the Z80's tri-state bus because the auto-sensing level shifters couldn't cope with floating signals.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tinycomputers.io/posts/redesigning-a-pcb-with-claude-code-and-open-source-eda-part-1.html"&gt;Redesigning a PCB with Claude Code and Open-Source EDA Tools (Part 1)&lt;/a&gt; — the v0.2 redesign. Replaced the TXB0108 auto-sensing shifters with SN74LVC8T245 driven shifters, generated the PCB programmatically from a Python script, autorouted, and shipped Gerbers to fabrication. (The "Part 1" in its title was meant to lead directly into a Part 2 about assembly and bring-up — but a bug derailed that plan.)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tinycomputers.io/posts/how-a-pin-numbering-bug-killed-a-pcb.html"&gt;How a Pin Numbering Bug Killed a PCB&lt;/a&gt; — the unplanned post-mortem on why the v0.3 board didn't work. A pin-numbering convention mismatch on the dual-row headers put every signal at the wrong physical position. The fabricated board was perfect; the design was wrong.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This post is about the v0.4 respin. It is also an honest accounting of what AI-assisted PCB design actually looks like in practice: the places where the workflow is miraculous, the places where it is agonizing, and the hour-long arguments between Freerouting and pcb-rnd about whether two pieces of copper were three tenths of a millimeter apart.&lt;/p&gt;
&lt;p&gt;If you came here looking for a triumphant "I built a PCB with AI and it just worked" story, this is not that post. If you came here looking for a nuanced report from someone who spent days on this and now has calibrated opinions about where it's worth doing, welcome.&lt;/p&gt;
&lt;h3&gt;The Setup, Briefly&lt;/h3&gt;
&lt;p&gt;For readers who haven't read the earlier posts: the GigaShield is a 155mm x 90mm PCB that sits between an Arduino Giga R1 (3.3V logic) and a RetroShield Z80 (5V logic). It has ten SN74LVC8T245PW level-shifter ICs translating 72 channels between the two voltage domains. The Giga plugs into the bottom (3.3V headers), the RetroShield plugs into the top (5V headers), and the shifters bridge the two.  Our first design used TXB0108 level-shifter ICs but the autosensing signal direction did not play nicely with the Z80's tri-state signals.&lt;/p&gt;
&lt;p&gt;The entire PCB is generated by a Python script. Not the schematic — there is no schematic. Not the layout — there is no graphical layout. The script emits a &lt;a href="https://baud.rs/1J64T5"&gt;pcb-rnd&lt;/a&gt; board file directly: component placements, pad definitions, netlist, board outline, silkscreen text, all in text format. Running &lt;code&gt;python3 build_giga_shield.py&lt;/code&gt; produces &lt;code&gt;giga_shield.pcb&lt;/code&gt; in a fraction of a second. The board is then routed by &lt;a href="https://baud.rs/freer"&gt;Freerouting&lt;/a&gt; and exported to Gerbers via pcb-rnd's command-line tools.&lt;/p&gt;
&lt;p&gt;Everything happens in the terminal. No GUIs, no mouse clicks, no "did I save the layout?" anxiety.&lt;/p&gt;
&lt;p&gt;The v0.3 board — subject of the previous post — failed because of a subtle bug in this pipeline. The v0.4 board fixes that bug and several others I didn't know about. This post covers what it took to find and fix them.&lt;/p&gt;
&lt;h3&gt;Bug Class #1: The Kind AI Caught Easily&lt;/h3&gt;
&lt;p&gt;Before refabricating, I asked Claude Code to audit the Python generator for any additional bugs. I framed it as a careful code review with specific checkpoints: pin numbering in both the pcb-rnd and KiCad generators, SN74LVC8T245 pin mapping against the TI datasheet, critical Z80 signal routing, and component placement validation.&lt;/p&gt;
&lt;p&gt;The audit took maybe fifteen minutes of wall-clock time — during which Claude Code read the entire codebase, cross-referenced it against the datasheet, and produced a severity-graded report. Most findings were cosmetic (stale comments, a print statement that said "1x10 header" for an 11-pin connector, harmless inconsistencies between the two build scripts). One finding was genuinely important:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;H1 — Comment "default A→B" is factually wrong, and U10's pulldown produces incorrect default behavior without user intervention.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;DIR=LOW per the SN74LVC8T245 datasheet means B→A (B-side drives A-side). The comment states the opposite. More importantly, U10 uses DIR for CLK, RESET, INT, NMI — signals that must flow A→B (Giga→Z80). With R10 pulling DIR_U10 to GND, U10 powers up in the B→A direction, which means the 5V Z80 side would drive the 3.3V Giga side on these control pins. Backwards and potentially damaging if the Z80 is outputting signals while the Giga is also driving those pins.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This bug is worth dwelling on, because it highlights something important about how I've been working on this project. I didn't write that code. Claude Code did, three weeks earlier, as part of the initial generator script. The wrong comment and the wrong pulldown direction were both introduced by the same LLM that later caught the mistake on audit.&lt;/p&gt;
&lt;p&gt;That sounds like it should be embarrassing for the workflow — the tool that generates bugs is also the tool that reviews them — but I think it's actually the right shape of the argument. A single LLM pass is fallible. A single human pass is also fallible. What matters is whether the combined system catches bugs reliably before they ship. In this case, the first pass (generation) introduced a subtle error, the second pass (audit, explicitly framed as a datasheet-cross-referencing review) caught it. The human's job was to know that the second pass was worth requesting — and to recognize, when the audit report came back, which findings mattered and which were cosmetic.&lt;/p&gt;
&lt;p&gt;This is exactly the kind of bug that a human engineer might catch on a good day and miss on a bad day. The comment and the circuit were internally consistent but both wrong. The datasheet was authoritative but three hundred pages away. Claude Code's audit didn't find it by being clever — it found it by mechanically cross-referencing every pin mapping against the datasheet, every comment against the actual behavior, with the patience of a machine that doesn't get bored.&lt;/p&gt;
&lt;p&gt;The fix was a one-line change: swap R10 from a pulldown-to-GND to a pullup-to-+3V3. U10 now powers up in the correct direction without any firmware intervention.&lt;/p&gt;
&lt;p&gt;I'll be transparent: the wrong direction would not have destroyed the boards immediately. The 5V CMOS outputs driving a 3.3V CMOS input is within the chips' absolute maximum ratings for short periods, especially if the Giga's pins are configured as inputs during that window. But in the steady state, with the Arduino trying to drive those pins as outputs, you'd have a lot of current flowing through ESD diodes, probably latch-up, almost certainly failure. A bug that would have cooked several \$70 Arduino Giga R1s over the life of the project.&lt;/p&gt;
&lt;p&gt;Fifteen minutes of LLM review. One avoided burn-up. A comfortable argument for the workflow.&lt;/p&gt;
&lt;h3&gt;Bug Class #2: The Kind AI Didn't Help With At All&lt;/h3&gt;
&lt;p&gt;Then came the Freerouting clearance saga.&lt;/p&gt;
&lt;p&gt;After the v0.4 build script was fixed, regenerated, and re-routed with Freerouting at the familiar 0.254mm trace width and 0.254mm clearance rule, I packaged up the Gerbers and uploaded them to &lt;a href="https://baud.rs/youwpy"&gt;PCBWay&lt;/a&gt;. Their automated DFM check failed almost immediately with a message I'd never seen before:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Failed reason: The spacing between copper traces and pads should be larger than 0.1mm.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A few hours later, a QA engineer at PCBWay followed up with a screenshot from their internal Gerber inspection tool. They had loaded the top-copper layer, zoomed into the crowded region around the level-shifter ICs, and drawn yellow arrows at eight or ten spots where traces were running uncomfortably close to pads. At one spot they'd highlighted in cyan, their measurement tool showed &lt;code&gt;D=0&lt;/code&gt; — literal zero distance between a trace and a pad.&lt;/p&gt;
&lt;div style="text-align: center; margin: 30px 0;"&gt;
&lt;img src="https://tinycomputers.io/images/giga-shield/pcbway-dfm-violations.png" alt="PCBWay QA engineer's Gerber inspection screenshot — yellow arrows mark violation spots, cyan measurement shows D=0 between a trace and a pad" style="max-width: 100%; border: 1px solid #ddd; border-radius: 8px;"&gt;
&lt;p style="color: #666; font-size: 12px; margin-top: 10px;"&gt;The screenshot PCBWay's QA engineer sent back. Yellow arrows mark the violations. Cyan highlight shows their measurement tool reporting D=0 — a trace touching a pad that it shouldn't be touching.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;This should not have been possible. Freerouting had been told to maintain 0.254mm clearance between all copper. 0.254mm is 10 mil, which is more than double PCBWay's 0.1mm minimum. If the router respected its rules, PCBWay's automated check should have sailed through.&lt;/p&gt;
&lt;p&gt;But it hadn't.&lt;/p&gt;
&lt;p&gt;I went through the usual debugging motions. Ran pcb-rnd's DRC on the routed board: 121 clearance violations. Opened the board in a gerber viewer: confirmed the violations were real, not artifacts. Tightened Freerouting's clearance to 0.3mm, narrowed traces to 0.2mm to give more room, added explicit clearance rules for every object-pair type (wire-pin, wire-via, smd-pin, pin-pin, and so on):&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m m-Double"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clearance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m m-Double"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clearance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m m-Double"&gt;0.3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;smd_smd&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clearance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m m-Double"&gt;0.3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;smd_via&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clearance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m m-Double"&gt;0.3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;smd_pin&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clearance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m m-Double"&gt;0.3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pin_pin&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clearance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m m-Double"&gt;0.3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pin_via&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clearance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m m-Double"&gt;0.3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;via_via&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clearance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m m-Double"&gt;0.3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;wire_wire&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clearance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m m-Double"&gt;0.3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;wire_via&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clearance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m m-Double"&gt;0.3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;wire_pin&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clearance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m m-Double"&gt;0.3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;wire_smd&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;Re-ran Freerouting. Ninety minutes of autorouting and optimization later, pcb-rnd's DRC still reported 121 clearance violations. Identical count. As if the clearance setting had been ignored entirely.&lt;/p&gt;
&lt;p&gt;This went on for several iterations. I tried 0.35mm clearance. 0.4mm clearance. I made the trace narrower. I made it wider. I added explicit rules for each layer. The violation count remained 121. Freerouting's optimization phase consistently took ninety minutes and improved the design by "about 52%" each time. The DRC report was unmoved.&lt;/p&gt;
&lt;p&gt;Claude Code was with me through all of this. It read the Freerouting log. It parsed the DRC output. It spotted the &lt;code&gt;p_shape is not bounded&lt;/code&gt; warnings (only three occurrences, probably unrelated). It suggested hypotheses — maybe Freerouting measured clearance from via hole centers rather than copper rings, maybe the padstack path shapes were confusing the bounds calculation, maybe pcb-rnd's DRC was using a threshold different from what Freerouting was told. Each hypothesis was plausible. None of them panned out. The violation count stayed at 121.&lt;/p&gt;
&lt;p&gt;The breakthrough came not from any clever insight but from a dumb experiment: run the DRC on the &lt;em&gt;unrouted&lt;/em&gt; board. Zero violations. Run it after Freerouting: 121 violations. Same 121, every time. Re-route with completely different settings: still 121 violations at roughly the same coordinates.&lt;/p&gt;
&lt;p&gt;The coordinates were the tell. The 121 "violations" weren't scattered — they were clustered at specific, consistent locations. And those locations, when I finally examined them carefully, were pin-to-trace junctions. Where a trace ended at a pad on its own net. Where the copper legitimately overlapped because they were the same electrical node.&lt;/p&gt;
&lt;p&gt;pcb-rnd's DRC was flagging every legitimate trace-pad connection as a "shorted nets: net too close to other net" violation. Not because the nets were shorted, but because pcb-rnd's DRC algorithm didn't properly account for the fact that two pieces of copper on the &lt;em&gt;same&lt;/em&gt; net are supposed to touch. They're supposed to be connected. That's the whole point.&lt;/p&gt;
&lt;p&gt;All 121 "violations" were false positives. Freerouting had been maintaining 0.3mm clearance the whole time. pcb-rnd had been lying about it the whole time. And PCBWay's automated DFM had been flagging the real issue — spots where same-net connections appeared to violate clearance under their algorithm — except their tool was smart enough to usually recognize same-net connections, so it flagged only the ones where the overlap happened to look particularly bad in the Gerber rendering.&lt;/p&gt;
&lt;p&gt;The fix, once I understood this, was to increase clearance (which I'd been doing) until the visual overlap was conservative enough that PCBWay's tool stopped complaining, and stop trusting pcb-rnd's DRC entirely. The winning configuration was 0.254mm traces with 0.3mm clearance and the explicit per-type clearance rules. PCBWay's DFM passed on that submission.&lt;/p&gt;
&lt;p&gt;Total time spent on this: maybe six hours across two days, counting the autorouting time, the iteration loops, and the eventual diagnosis. Nothing Claude Code did made this faster. Claude Code could parse logs and suggest hypotheses as quickly as I could read them, but it couldn't see what I couldn't see. The problem wasn't in any file — it was in the interaction between three tools' different assumptions about what "clearance" means. That kind of bug lives in the interfaces, not in any single artifact. LLMs are not good at debugging interfaces they can't run.&lt;/p&gt;
&lt;p&gt;I don't think this is a damning critique of AI-assisted workflows. But it is a calibrating one. If you're choosing between "write a Python script and fight Freerouting and pcb-rnd and PCBWay's DFM" versus "click around in KiCad for four hours," the second path has fewer interfaces to break. Graphical tools eat their own complexity internally. The CLI workflow exposes every seam.&lt;/p&gt;
&lt;h3&gt;Bug Class #3: The Kind AI Made Worse&lt;/h3&gt;
&lt;p&gt;At some point during the clearance debugging, I decided to reduce the board from 6 layers to 4 layers. Freerouting had been routing successfully on four, so paying for six was wasteful. I updated the pcb-rnd build script to emit a 4-layer stack, changed the Groups() directive to &lt;code&gt;"1,c:2:3:4,s:5:6:7"&lt;/code&gt;, regenerated, exported DSN, routed, imported, exported Gerbers.&lt;/p&gt;
&lt;p&gt;The resulting Gerber package had five copper layers.&lt;/p&gt;
&lt;p&gt;Not six. Not four. &lt;em&gt;Five.&lt;/em&gt; With traces scattered across them in a way that suggested pcb-rnd, on &lt;code&gt;SaveTo&lt;/code&gt;, had rewritten my Groups() string into something of its own invention — &lt;code&gt;"6:8:1,c:2:3:5:10:11:4,s:9:7"&lt;/code&gt; — that created phantom layers and assigned them roles my build script hadn't intended. The gerber named &lt;code&gt;intern.copper.none.12.gbr&lt;/code&gt; had 1,406 traces. This was not a layer I had defined.&lt;/p&gt;
&lt;p&gt;I asked Claude Code to help me understand pcb-rnd's Groups() syntax. It gamely tried to parse the string. It offered three different interpretations, each of which would have produced a different layer stack than what pcb-rnd actually did. It couldn't fix the problem because it couldn't run pcb-rnd and observe the behavior. I couldn't fix it either, for the same reason with more forgivable excuses.&lt;/p&gt;
&lt;p&gt;After maybe two hours of going in circles, I reverted to the original 6-layer Groups() string, re-routed, exported Gerbers (getting six copper gerbers — four with traces, two with only padstack pads), and deleted the two empty ones before zipping. PCBWay doesn't care what my source PCB file says. They fabricate what's in the Gerber bundle. If I submit four copper layers, I get a 4-layer board.&lt;/p&gt;
&lt;p&gt;This is a pattern worth noting. Sometimes the right solution to a tool-chain problem isn't to fix the tool chain. It's to work around it. Claude Code is good at helping you solve problems correctly; it's not as good at helping you recognize that the correct solution is to stop trying to solve the problem. That's a uniquely human skill: knowing when to quit.&lt;/p&gt;
&lt;h3&gt;The Component Placement Lesson&lt;/h3&gt;
&lt;p&gt;One more story worth telling. The v0.3 design had the ten level-shifter ICs arranged as U1-U5 across the top of the board and U6-U10 stacked in a single vertical column on the right side. It looked tidy. It routed successfully on six layers. It also concentrated an enormous amount of signal traffic into a narrow vertical channel between the shifter cluster and the 2x18 headers — so narrow that Freerouting couldn't route all 308 nets on four layers without leaving some unrouted.&lt;/p&gt;
&lt;p&gt;Claude Code flagged this the first time I tried the 4-layer route. Not by proposing a new placement — it didn't. By noting that the unrouted nets all terminated in the same congested region, and asking whether I'd considered spreading U6 through U10 into a staggered two-column layout between J9 and J10. I hadn't. The suggestion was obvious in hindsight, which is the clearest sign that it was useful. A lot of engineering is exactly this: you know the answer once someone mentions it, but no one mentions it and you don't think to ask yourself.&lt;/p&gt;
&lt;p&gt;The new placement — U6, U8, U9 in one column closer to J9, U7 and U10 offset in another column closer to J10 — routed cleanly on four layers with 314/314 nets connected. The LLM didn't solve a geometric problem I couldn't solve. It pointed at a geometric problem I hadn't recognized as a problem. That's a different and narrower contribution than "AI designed my board," but it's also real and repeatable.&lt;/p&gt;
&lt;p&gt;For the curious: my mental model for why the original layout was bad had been "I want them in a tidy column near the connector." The LLM's implicit mental model, derived from having read every PCB design textbook ever digitized, was "signal traffic wants to spread, not concentrate." Both are defensible. The second is more useful.&lt;/p&gt;
&lt;h3&gt;What Actually Works&lt;/h3&gt;
&lt;p&gt;Stepping back from the debugging, here is my honest breakdown of where Claude Code earned its keep on this project versus where it didn't.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Where it was essentially indispensable:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Parsing hierarchical S-expression formats.&lt;/strong&gt; The original KiCad design from &lt;a href="https://tinycomputers.io/posts/fiverr-pcb-design-arduino-giga-shield.html"&gt;the Fiverr engineer&lt;/a&gt; was a &lt;code&gt;.kicad_sch&lt;/code&gt; file with nested sheets, positional net labels, and implicit connections across hierarchical boundaries. Extracting the 72-channel signal mapping from that file would have taken me half a day by hand and maybe an hour of Python if I were patient. Claude Code did it in about twenty minutes of interactive conversation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Generating boilerplate code with domain-specific constraints.&lt;/strong&gt; The &lt;code&gt;tssop24_element()&lt;/code&gt; function that produces the SN74LVC8T245PW footprint is three dozen lines of boring arithmetic — pad positions, coordinate transforms, string formatting. I asked for a pcb-rnd Element that matched the SN74LVC8T245PW footprint and got working code on the first try — Claude Code fetched the TI datasheet, read the pcb-rnd format reference, and produced the correct geometry without me having to hand it either document. I would have gotten it wrong on the first try if I'd written it by hand, because I would have flipped the B-side pin order (pins 13-24 run bottom-to-top on this package, which is easy to miss).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cross-referencing mechanical information against specifications.&lt;/strong&gt; The audit I mentioned earlier — reading my entire codebase and checking it against the TI datasheet — would have been tedious and error-prone to do manually. Claude Code did it reliably and caught a bug I would have shipped.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;File format debugging.&lt;/strong&gt; pcb-rnd's error messages are unhelpful by the standards of modern software. The Specctra DSN format has undocumented quirks. Gerber apertures have version-specific formatting differences. Every time I hit an error I didn't understand, Claude Code could read the file, diff it against a working example, and tell me what was different. This is LLM work at its best — bulk pattern-matching across reference material I don't have memorized.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Bridging the SSH workflow.&lt;/strong&gt; pcb-rnd runs on Linux and I work on a Mac. Half of the pipeline was shell commands to upload files, run pcb-rnd on a remote machine, download results, and iterate. Claude Code managed that SSH shuffle cleanly across hundreds of invocations. Not a hard problem, but a tedious one. Delegating it was valuable.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Where it didn't help:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Diagnosing cross-tool interaction bugs.&lt;/strong&gt; The 121-false-positive clearance saga was a problem that lived between three tools' different mental models. Claude Code could read each tool's output but couldn't run experiments to validate hypotheses. I had to do that myself, slowly.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Making placement decisions involving physical intuition.&lt;/strong&gt; Claude Code helped with the U6-U10 restacking only after I'd ruled out other options. It didn't lead the design choice. For pure physical intuition — "this will be too dense," "this trace will pick up noise from this inductor," "this via is a mechanical weakness" — the LLM was helpful as a second opinion but not a primary source.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Knowing when to stop debugging and accept a workaround.&lt;/strong&gt; When pcb-rnd's &lt;code&gt;SaveTo&lt;/code&gt; mangled my layer stack, Claude Code happily kept trying to fix it. It took me longer than it should have to realize the fix was to not use &lt;code&gt;SaveTo&lt;/code&gt;. LLMs are optimized for "helpfully continue the task." Human judgment is needed for "recognize the task is wrong."&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;The Economics&lt;/h3&gt;
&lt;p&gt;It's fair to ask whether any of this is worth it. I'll try to be honest.&lt;/p&gt;
&lt;p&gt;Time invested in the v0.4 respin, from identifying the pin-numbering bug through submitting the second Gerber package:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Designing the fix: 2 hours (including the code audit that caught R10)&lt;/li&gt;
&lt;li&gt;Regenerating and re-routing: ~4 hours of wall-clock time, mostly Freerouting optimization&lt;/li&gt;
&lt;li&gt;The clearance debugging saga: ~6 hours&lt;/li&gt;
&lt;li&gt;The layer-stack misadventure: ~2 hours&lt;/li&gt;
&lt;li&gt;Documentation, git commits, this blog post: ~4 hours&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Total: ~18 hours of my engineering time, plus a lot of Freerouting CPU-hours.&lt;/p&gt;
&lt;p&gt;If I had paid a Fiverr freelancer to redo the design in KiCad from scratch, it would have cost about \$400 and taken about a week of wall-clock time. If I had learned KiCad properly and done the layout myself, it would have taken probably 20 to 30 hours for a 314-net 4-layer board, given I've never used KiCad's layout editor seriously. If I had used a commercial autorouter (Altium's, say), I would have paid thousands of dollars in licensing and probably spent 10 hours on the project.&lt;/p&gt;
&lt;p&gt;The Python-and-Claude-Code workflow cost me 18 hours of engineering time and the frustration of debugging interactions between three different CLI tools. PCBWay sponsored the fabrication, so the direct cash outlay was zero on this project — but that's an artifact of the sponsorship, not the workflow. A non-sponsored hobbyist running the same pipeline would spend roughly \$50 per prototype fabrication run at PCBWay's standard rates, so two runs (v0.3 plus v0.4) would be in the ballpark of \$100 total. The boards will arrive next week.&lt;/p&gt;
&lt;p&gt;Is that a good trade? It depends on what you value. For me, the workflow is repeatable — the next board I design will take a fraction of this effort because the tool chain is now debugged. For someone doing one PCB in their lifetime, this would be a terrible trade. For someone who plans to iterate on a design family over months or years, it's probably a good one. The upfront cost amortizes.&lt;/p&gt;
&lt;p&gt;I don't think the right question is "AI-assisted PCB workflow yes or no." I think it's "what kind of PCB work are you doing, and does your workflow compound?" If your boards are one-offs, use KiCad. If your boards are a family that evolves over time, scripted workflows start paying for themselves around the third iteration. Add AI assistance on top and the break-even point moves earlier.&lt;/p&gt;
&lt;h3&gt;What I'd Do Differently&lt;/h3&gt;
&lt;p&gt;A few hard-won principles from this project, in case anyone tries something similar:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Validate the pipeline on a dead-simple board before using it for a real one.&lt;/strong&gt; If I had generated a toy two-net board first, routed it, exported Gerbers, and submitted the files to PCBWay's online DFM check as a dry run, I would have caught the pin-numbering convention bug without wasting a fabrication run on v0.3. The total time for the dry run would have been maybe two hours. It would have saved a two-week turnaround — and, more importantly, not burned a sponsored fabrication run on a design that was never going to work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Run DRC with multiple tools and compare.&lt;/strong&gt; pcb-rnd's DRC was actively misleading on this board. If I had also run the Gerbers through a third-party tool like &lt;a href="https://gerber-viewer.com/"&gt;Gerber Viewer&lt;/a&gt; or through KiCad's DRC after importing, I'd have noticed the disagreement and dug into it earlier. Never trust a single tool's DRC as authoritative.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep PCBWay's DFM as the source of truth.&lt;/strong&gt; Their automated check has seen more boards than any open-source DRC. It's tuned for actual manufacturability. When the open-source tools say "clean" and PCBWay says "fail," believe PCBWay.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Write test fixtures for the scripted generator.&lt;/strong&gt; A few unit tests that verify "net X has pins Y and Z" would have caught the pin-numbering bug in the build step rather than the fabrication step. For a scripted PCB workflow, the Python code needs the same test discipline as any other production code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Budget for LLM's failure modes.&lt;/strong&gt; The LLM is fast and confident but can spiral into unproductive debugging loops. When a fix doesn't work on the second or third try, that's the signal to stop and think rather than let the LLM keep trying variations. Six hours on the clearance bug should have been two.&lt;/p&gt;
&lt;h3&gt;The Broader Question&lt;/h3&gt;
&lt;p&gt;There's a cultural current in software circles right now that frames AI coding assistants as either revolutionary or fraudulent. Neither frame captures what I experienced on this project.&lt;/p&gt;
&lt;p&gt;Claude Code didn't replace my expertise. I still had to know what a level shifter is, why the Z80 tri-states its bus during IO cycles, why the annular ring on a via matters for fab yield, when a pull-up is safer than a pull-down. Without that domain knowledge, I couldn't have directed Claude Code at the right problems, and I couldn't have recognized when its suggestions were wrong.&lt;/p&gt;
&lt;p&gt;Claude Code also didn't slow me down. The audit that caught the R10 bug was pure leverage. The file-format debugging was pure leverage. The SSH shuffle was pure leverage. The CLI workflow I'm using would not be tractable without an LLM assistant — too many file formats, too many tools, too much boilerplate. Claude Code didn't enable the workflow, but it made it something I could actually use instead of abandoning it for KiCad's GUI after the first pcb-rnd error message.&lt;/p&gt;
&lt;p&gt;What Claude Code is, for PCB design, is a competent junior collaborator with encyclopedic memory, infinite patience, and no physical intuition. It won't design your board for you. It'll help you design your board, if you know what you're doing. That's a different and less exciting claim than the marketing suggests, but it's also a more durable one.&lt;/p&gt;
&lt;h3&gt;What's Next&lt;/h3&gt;
&lt;p&gt;The v0.4 boards are scheduled to arrive from &lt;a href="https://baud.rs/youwpy"&gt;PCBWay&lt;/a&gt; in a few weeks. If they work — if the Z80 actually responds to its clock, if the data bus reads are clean, if &lt;code&gt;/IORQ&lt;/code&gt; is no longer stuck at ground — I'll write a Part 4 covering the bring-up and the test results. If they don't work, I'll write a Part 4 about whatever new bug I've introduced.&lt;/p&gt;
&lt;p&gt;In the meantime, the Python build script, pcb-rnd source files, Gerber outputs, Arduino test sketch, and every piece of infrastructure discussed in these posts is open source:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/ajokela/giga-shield"&gt;giga-shield&lt;/a&gt;&lt;/strong&gt; — Complete design files, build pipeline, and test firmware&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you're interested in scripted PCB design workflows, I'd genuinely like to hear from people who've tried similar approaches — or, more interestingly, tried and given up. The body of public literature on "I attempted this and it didn't work for me" is much smaller than on "I succeeded, here's how," and I think the former is more useful.&lt;/p&gt;
&lt;p&gt;*Previous posts in this series: &lt;a href="https://tinycomputers.io/posts/redesigning-a-pcb-with-claude-code-and-open-source-eda-part-1.html"&gt;Redesigning with Claude Code (Part 1)&lt;/a&gt; · &lt;a href="https://tinycomputers.io/posts/how-a-pin-numbering-bug-killed-a-pcb.html"&gt;How a Pin Numbering Bug Killed a PCB&lt;/a&gt; · &lt;a href="https://tinycomputers.io/posts/fiverr-pcb-design-arduino-giga-shield.html"&gt;Fiverr PCB Design (\$468)&lt;/a&gt; &lt;/p&gt;</description><category>ai</category><category>arduino</category><category>arduino giga</category><category>claude code</category><category>freerouting</category><category>hardware</category><category>level shifter</category><category>open-source</category><category>pcb design</category><category>pcb-rnd</category><category>pcbway</category><category>retroshield</category><category>z80</category><guid>https://tinycomputers.io/posts/what-routing-314-nets-taught-me-about-ai-assisted-pcb-design.html</guid><pubDate>Sun, 19 Apr 2026 23:00:00 GMT</pubDate></item><item><title>How a Pin Numbering Bug Killed a PCB</title><link>https://tinycomputers.io/posts/how-a-pin-numbering-bug-killed-a-pcb.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/how-a-pin-numbering-bug-killed-a-pcb_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;30 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;div class="sponsor-widget"&gt;
&lt;div class="sponsor-widget-header"&gt;&lt;a href="https://baud.rs/youwpy"&gt;&lt;img src="https://tinycomputers.io/images/pcbway-logo.png" alt="PCBWay" style="height: 22px; vertical-align: middle; margin-right: 8px;"&gt;&lt;/a&gt; Sponsored Hardware&lt;/div&gt;
&lt;p&gt;The boards in this post were fabricated by &lt;a href="https://baud.rs/youwpy"&gt;PCBWay&lt;/a&gt;, who sponsored the GigaShield v0.3 level converter project. PCBWay offers PCB prototyping, assembly, CNC machining, and 3D printing services with turnaround times starting at 24 hours. Whether you're prototyping a single board or scaling to production, check them out at &lt;a href="https://baud.rs/youwpy"&gt;pcbway.com&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;&lt;img src="https://tinycomputers.io/images/giga-shield/giga-shield-v03-board.jpeg" alt="GigaShield v0.3 PCB — black solder mask, ten SN74LVC8T245PW level shifters, fabricated by PCBWay" style="float: right; max-width: 420px; margin: 0 0 1em 1.5em; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);"&gt;&lt;/p&gt;
&lt;p&gt;The boards arrived from &lt;a href="https://baud.rs/youwpy"&gt;PCBWay&lt;/a&gt; in perfect condition. Black solder mask, clean silkscreen, precise drill hits. The fabrication quality was excellent — 6-layer board, 6/6 mil trace/space, HASL finish, delivered in about two weeks from order to doorstep. PCBWay's online Gerber viewer had flagged one component overlap before manufacturing (a decoupling capacitor crowding a level shifter IC), their team asked about it, and we resolved it in a single email exchange. Everything about the fabrication was smooth.&lt;/p&gt;
&lt;p&gt;Then I plugged in the &lt;a href="https://baud.rs/87wbBL"&gt;RetroShield Z80&lt;/a&gt;, uploaded a test sketch to the &lt;a href="https://baud.rs/poSQeo"&gt;Arduino Giga R1&lt;/a&gt;, and nothing worked.&lt;/p&gt;
&lt;p&gt;Not "mostly worked with some issues." Nothing. The Z80 didn't respond to its clock. The data bus read all zeros. One control signal — &lt;code&gt;/IORQ&lt;/code&gt; — was stuck permanently LOW while the others sat HIGH. Sixteen bus cycles of silence where there should have been a Z80 booting up and fetching instructions from address 0x0000.&lt;/p&gt;
&lt;p&gt;The board wasn't defective. PCBWay had fabricated exactly what I asked them to fabricate. The problem was that what I asked them to fabricate was wrong.&lt;/p&gt;
&lt;h3&gt;The GigaShield v0.3&lt;/h3&gt;
&lt;p&gt;For context: the GigaShield is a level-shifter shield that sits between an Arduino Giga R1 (3.3V logic) and a RetroShield Z80 (5V logic). It has ten SN74LVC8T245PW 8-channel bidirectional level translators, translating 72 signal channels between the two voltage domains. The design was &lt;a href="https://tinycomputers.io/posts/redesigning-a-pcb-with-claude-code-and-open-source-eda-part-1.html"&gt;covered in detail in Part 1&lt;/a&gt; of this series — the short version is that the entire PCB was generated programmatically from a Python script, exported to &lt;a href="https://baud.rs/1J64T5"&gt;pcb-rnd&lt;/a&gt; format, autorouted with &lt;a href="https://baud.rs/wdr0dP"&gt;Quilter.ai&lt;/a&gt;, and sent to PCBWay as Gerber files.&lt;/p&gt;
&lt;p&gt;The board has twelve connectors. Ten are single-row pin headers (1xN) for the standard Arduino shield headers, the analog breakout, and a direction-control header. Two are dual-row headers (2x18) that carry the high-pin-count digital signals — Arduino digital pins 22 through 53 on one side, level-shifted to 5V on the other.&lt;/p&gt;
&lt;p&gt;Those two dual-row headers are where the bug lives.&lt;/p&gt;
&lt;h3&gt;First Contact&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/giga-shield/giga-shield-stack.jpeg" alt="Full test stack: Arduino Giga R1 with GigaShield level converter and RetroShield Z80, connected with jumper wires for DIR control" style="float: right; max-width: 420px; margin: 0 0 1em 1.5em; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);"&gt;&lt;/p&gt;
&lt;p&gt;The first test was simple: read the Z80's control lines with no clock running. The SN74LVC8T245 level shifters have explicit direction control (that's why I &lt;a href="https://tinycomputers.io/posts/redesigning-a-pcb-with-claude-code-and-open-source-eda-part-1.html"&gt;chose them over the TXB0108&lt;/a&gt;), so with the direction defaulting to B-to-A (5V side drives, Arduino reads), I should see the Z80's active-low control outputs all sitting HIGH — their idle state.&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;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Control&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Z80&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;signals&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="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;M1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;RD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;WR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;MREQ&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;IORQ&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;With&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Z80&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;should&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HIGH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inactive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Four out of five correct. &lt;code&gt;/IORQ&lt;/code&gt; reading LOW was the first clue that something was wrong, but I initially dismissed it as a possible floating pin or a pull-down issue on the RetroShield side.&lt;/p&gt;
&lt;p&gt;The real alarm came when I tried to boot the Z80. The test sketch drives CLK, releases RESET, and captures the first 16 bus cycles:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;=== Test 5: Z80 boot — first 16 bus cycles ===
  RESET released
  T00: /M1=1 /RD=1 /MREQ=1 DATA=0x00
  T01: /M1=1 /RD=1 /MREQ=1 DATA=0x00
  T02: /M1=1 /RD=1 /MREQ=1 DATA=0x00
  ...
  T15: /M1=1 /RD=1 /MREQ=1 DATA=0x00
  RESET asserted — Z80 halted
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Sixteen clock cycles, and the Z80 never responded. No &lt;code&gt;/M1&lt;/code&gt; going low for an opcode fetch. No &lt;code&gt;/MREQ&lt;/code&gt; for a memory request. No &lt;code&gt;/RD&lt;/code&gt; for a read cycle. The Z80 was either not getting a clock signal, or not getting a valid RESET sequence, or both.&lt;/p&gt;
&lt;p&gt;I checked the obvious things first. Is the 3.3V rail powered? Yes. Is the 5V rail powered? Yes. Is the direction control for U10 (the control-output shifter carrying CLK) set correctly? Yes — J11 pin 10 tied to 3.3V, which sets DIR HIGH for A-to-B (Giga drives Z80). Is the RetroShield seated properly? Yes.&lt;/p&gt;
&lt;h3&gt;Looking at the Schematic&lt;/h3&gt;
&lt;p&gt;With solder bridges ruled out and the electrical fundamentals verified, I went back to the Python build script — the single source of truth for the entire PCB design.&lt;/p&gt;
&lt;p&gt;The board's ten single-row headers all worked. The level shifters were passing signals correctly (the control input test proved that U9 was translating). The problem was specific to the Z80 not receiving CLK and RESET, and &lt;code&gt;/IORQ&lt;/code&gt; being stuck at ground.&lt;/p&gt;
&lt;p&gt;All three of those signals route through the 2x18 dual-row headers: J9 (3.3V side) and J10 (5V side). I started tracing nets.&lt;/p&gt;
&lt;p&gt;The build script generates pin headers with a function called &lt;code&gt;pin_header_element&lt;/code&gt;. For a 2x18 header, it iterates:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ncols&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;      &lt;span class="c1"&gt;# 0, then 1&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;   &lt;span class="c1"&gt;# 0 through 17&lt;/span&gt;
        &lt;span class="n"&gt;pin_num&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This produces &lt;strong&gt;column-first&lt;/strong&gt; numbering: pins 1–18 run down column 0, pins 19–36 run down column 1.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;Col 0         Col 1
Pin 1         Pin 19
Pin 2         Pin 20
Pin 3         Pin 21
...           ...
Pin 18        Pin 36
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;But the net arrays that assign signals to pin numbers were written in &lt;strong&gt;zigzag&lt;/strong&gt; order — the standard convention for dual-row pin headers, where pin numbers alternate between columns:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;Col 0         Col 1
Pin 1         Pin 2
Pin 3         Pin 4
Pin 5         Pin 6
...           ...
Pin 35        Pin 36
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The J9 net array:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;j9&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'+5V'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'+5V'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'D22'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'D23'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'D24'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'D25'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
      &lt;span class="s1"&gt;'D52'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'D53'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'GND'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'GND'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This assumes zigzag: pin 3 = D22 at (col 0, row 1), pin 4 = D23 at (col 1, row 1). But the footprint generator puts pin 3 at (col 0, row 2) and pin 4 at (col 0, row 3) — same column, two rows apart instead of across from each other.&lt;/p&gt;
&lt;p&gt;Every signal on J9 and J10 was at the wrong physical position.&lt;/p&gt;
&lt;h3&gt;The Geometry of the Bug&lt;/h3&gt;
&lt;p&gt;To understand why this specific mismatch is catastrophic, consider what happens to a few critical signals.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;D52 (CLK):&lt;/strong&gt; In the net array, D52 is at index 32 (pin 33). In zigzag, pin 33 is at (col 0, row 16) — left column, second-to-last row. In column-first, pin 33 is at (col 1, row 14) — right column, six rows higher. The RetroShield's CLK pin physically touches the pad at zigzag position (col 0, row 16), but the GigaShield routed CLK to column-first position (col 1, row 14). Different column, different row. The Z80 never sees a clock.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;D53 (/IORQ):&lt;/strong&gt; Pin 34 in zigzag is at (col 1, row 16). In column-first, pin 34 is at (col 1, row 15) — one row off. But at the zigzag (col 1, row 16) position, the column-first numbering places pin 35, which the net array assigns to &lt;strong&gt;GND&lt;/strong&gt;. The RetroShield's &lt;code&gt;/IORQ&lt;/code&gt; pin is physically sitting on a ground pad. That's why it reads LOW — it's hard-wired to ground through the PCB trace.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;D38 (RESET):&lt;/strong&gt; Pin 19 in zigzag is at (col 1, row 9). In column-first, pin 19 is at (col 1, row 0) — the very first row of the second column instead of the middle. RESET goes to a completely unrelated position.&lt;/p&gt;
&lt;p&gt;The pattern holds for every signal on the 36-pin header. The first two pins (+5V, +5V) happen to be at matching positions for both conventions (pin 1 is always col 0 row 0, and pin 2's mismatch doesn't matter since both are power). After that, every signal diverges.&lt;/p&gt;
&lt;h3&gt;Why Nothing Caught It&lt;/h3&gt;
&lt;p&gt;This bug is invisible to every standard verification step in the PCB pipeline.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DRC (Design Rule Check):&lt;/strong&gt; All traces meet clearance and width rules. The traces connect the correct logical pin numbers — the netlist is internally consistent. DRC validates geometry, not pin-numbering conventions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Visual inspection:&lt;/strong&gt; The board renders look correct. Traces run from shifter pads to header pads in clean, routed paths. You can't tell from a PNG rendering that a header pad at row 14 should be at row 16. The footprint silkscreen shows pin 1, and the rest are just a grid of identical-looking through-holes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Quilter.ai:&lt;/strong&gt; The autorouter takes a netlist and routes traces between named pads. It has no concept of "this pad should be at this physical position" — it just connects pad A to pad B using copper. If the pad positions are wrong in the input file, the router dutifully routes to the wrong positions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Gerber review:&lt;/strong&gt; PCBWay's Gerber viewer (and any standard Gerber viewer) shows copper layers, drill hits, and silkscreen. It doesn't cross-reference pad positions against any external standard. The Gerbers were valid files describing a valid board — just not the board I intended.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;pcb-rnd:&lt;/strong&gt; The PCB editor displays the board as defined. It doesn't know that a 2x18 header should use zigzag numbering. It renders what the file says.&lt;/p&gt;
&lt;p&gt;The bug exists in the gap between two conventions: the net array assumes zigzag ordering (which is the industry standard for dual-row headers and what KiCad uses in its standard footprint library), while the footprint generator implements column-first ordering (a natural but incorrect choice when iterating &lt;code&gt;for col... for row...&lt;/code&gt;). Both halves are internally consistent. The error is in their interaction.&lt;/p&gt;
&lt;h3&gt;The One-Line Fix&lt;/h3&gt;
&lt;p&gt;The fix is almost comically small relative to the damage:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# Before (column-first — WRONG for standard dual-row headers):&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ncols&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;pin_num&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="c1"&gt;# After (zigzag — correct):&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ncols&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;pin_num&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Swap the loop order. That's it. Row-first iteration produces zigzag numbering: pin 1 at (col 0, row 0), pin 2 at (col 1, row 0), pin 3 at (col 0, row 1), pin 4 at (col 1, row 1), and so on. This matches the KiCad convention, the IPC convention, and what every dual-row connector in the world expects.&lt;/p&gt;
&lt;p&gt;Single-row headers (J1 through J8, J11) are unaffected because column-first and zigzag are identical when there's only one column. Only J9 and J10 — the two 2x18 headers — had the wrong pinout.&lt;/p&gt;
&lt;h3&gt;Can Software Fix It?&lt;/h3&gt;
&lt;p&gt;My first instinct was to work around the bug in firmware — remap which Arduino GPIO pins the sketch uses so that signals arrive at the correct physical positions despite the wrong traces. If the board routes D36 to where D52 should be, just use D36 for CLK in the sketch.&lt;/p&gt;
&lt;p&gt;It doesn't work, for two reasons.&lt;/p&gt;
&lt;p&gt;First, some signals map to power pins. D53 (&lt;code&gt;/IORQ&lt;/code&gt;) physically sits on a GND pad. You can't drive a signal through a ground trace in software. The pad is connected to the ground plane. It's not a GPIO — it's copper bonded to zero volts.&lt;/p&gt;
&lt;p&gt;Second, the level shifters have shared direction control. Each SN74LVC8T245 has eight channels and one DIR pin. All eight channels shift in the same direction. If you remap CLK (which needs Giga-to-Z80 direction) to go through a shifter that also carries address bus signals (which need Z80-to-Giga direction), you can't set both directions simultaneously. The shared DIR creates an unsolvable constraint when signals that need opposite directions land on the same shifter.&lt;/p&gt;
&lt;p&gt;The board needs a respin.&lt;/p&gt;
&lt;h3&gt;The Respin&lt;/h3&gt;
&lt;p&gt;With the bug identified and the fix trivial, the path forward is straightforward:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Fix the loop order in &lt;code&gt;pin_header_element&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Regenerate &lt;code&gt;giga_shield.pcb&lt;/code&gt; from the Python script&lt;/li&gt;
&lt;li&gt;Export to DSN format via pcb-rnd&lt;/li&gt;
&lt;li&gt;Re-route the traces&lt;/li&gt;
&lt;li&gt;Export new Gerbers&lt;/li&gt;
&lt;li&gt;Send to &lt;a href="https://baud.rs/youwpy"&gt;PCBWay&lt;/a&gt; for fabrication&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The entire pipeline — from fix to fabrication-ready Gerbers — takes about twenty minutes. That's the advantage of the text-based, scriptable workflow described in &lt;a href="https://tinycomputers.io/posts/redesigning-a-pcb-with-claude-code-and-open-source-eda-part-1.html"&gt;Part 1&lt;/a&gt;. Change one line of Python, re-run the pipeline, get a new board. No GUI interactions, no manual routing, no "did I remember to update the footprint" anxiety.&lt;/p&gt;
&lt;p&gt;The v0.3 board was originally routed on six layers because the autorouter couldn't find paths for every net with the components packed tightly together. For v0.4, we rearranged the level shifter ICs into a staggered two-column layout between the dual-row headers, giving the router more room to work with. The result: all 313 nets routed cleanly on just four layers. We're also using &lt;a href="https://baud.rs/bdZw62"&gt;Freerouting&lt;/a&gt; for the v0.4 routing — we had initially planned to use &lt;a href="https://baud.rs/wdr0dP"&gt;Quilter.ai&lt;/a&gt;, but their recent release introduced some parsing issues that made it unreliable for our KiCad files. Freerouting's v1.9 codepath, while older, has been rock-solid for this board.&lt;/p&gt;
&lt;p&gt;PCBWay's turnaround on prototype boards is fast — I've consistently gotten boards in under two weeks from order placement to delivery, including the &lt;a href="https://tinycomputers.io/posts/fiverr-pcb-design-arduino-giga-shield.html"&gt;original v0.1 boards from the Fiverr design&lt;/a&gt;. For the v0.4 respin, I'm using slighly different specs: 4-layer, 1.6mm FR-4, black solder mask, HASL finish, standard 6/6 mil trace/space. PCBWay's pricing for prototype quantities (5-10 boards) is genuinely hard to beat — and the quality has been consistently good across every order. &lt;/p&gt;
&lt;p&gt;One thing I appreciate about PCBWay's process: the pre-production review. Before they start cutting boards, their engineering team reviews the Gerbers and flags potential issues. They caught the C29/U10 overlap on the v0.3 boards — a decoupling capacitor footprint that crowded a TSSOP-24 IC. We agreed to leave C29 unpopulated (it was one of 29 bypass caps, not critical), and PCBWay proceeded with fabrication. That kind of proactive communication saves real time and money. If I'd caught the pin numbering bug at that stage, the whole issue would have been avoided. But pin numbering convention mismatches aren't the kind of thing that shows up in a Gerber review — the files were technically correct.&lt;/p&gt;
&lt;h3&gt;What PCBWay Offers&lt;/h3&gt;
&lt;p&gt;For readers who haven't used PCBWay before, a brief overview of what they provide beyond basic PCB fabrication:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PCB Prototyping:&lt;/strong&gt; 1 to 8 layers, multiple surface finishes (HASL, ENIG, OSP, immersion silver/tin), controlled impedance, blind/buried vias, flex and rigid-flex boards. Minimum trace/space of 3.5/3.5 mil for standard process. They handle both small prototype runs (5 boards) and production quantities.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PCB Assembly (PCBA):&lt;/strong&gt; Full turnkey assembly with component sourcing, SMT and through-hole placement, and testing. For a board like the GigaShield with thirty-six SMD components (ten TSSOP-24 ICs, twenty-seven 0603 caps, nine 0603 resistors), assembly service eliminates the most tedious part of the build. TSSOP-24 packages are hand-solderable with a fine-tip iron and flux, but doing ten of them with twenty-four 0.65mm-pitch pins each is several hours of careful work. PCBWay's pick-and-place machines do it in minutes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3D Printing and CNC Machining:&lt;/strong&gt; Useful for enclosures, mounting brackets, and custom mechanical parts. Multiple materials available — PLA, resin, nylon, aluminum, steel. I haven't used these services for this project, but for projects that need a custom case or mounting hardware, having it from the same vendor simplifies ordering.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stencil Service:&lt;/strong&gt; Solder paste stencils for reflow soldering. If you're doing your own assembly with a hot plate or reflow oven, a properly cut stencil makes paste application dramatically faster and more consistent than syringe application.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Design-for-Manufacturing (DFM) Review:&lt;/strong&gt; As mentioned above, PCBWay reviews your files before production and flags potential issues. This caught the C29 overlap on my boards. For someone iterating on a design — especially a design generated programmatically where visual review of the physical layout is less intuitive — this review is valuable.&lt;/p&gt;
&lt;p&gt;The pricing model scales well: prototype quantities are cheap enough to iterate without stress (important when you're, say, debugging a pin numbering convention), and production quantities get volume discounts. The online quoting system gives you a price instantly when you upload Gerbers, so you know the cost before committing.&lt;/p&gt;
&lt;h3&gt;Lessons&lt;/h3&gt;
&lt;p&gt;Every post-mortem needs a "what did we learn" section. Here's mine.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Test with hardware before ordering quantity.&lt;/strong&gt; If I'd breadboarded the 2x18 connection with jumper wires before committing to fabrication, I'd have caught the mismatch immediately. The single-row headers all work — I could have validated those and assumed the dual-row headers were fine. Testing the full signal path end-to-end, from Giga GPIO through the level shifter to the RetroShield's Z80, would have caught it in an hour.  One of the reasons I did not breadbroad the design first is I was unable to find breadboardable SN74LVC8T245PW level shifters.  I have &lt;a href="https://baud.rs/JyytXb"&gt;TXB0104 Bi-Directional Level Shifters&lt;/a&gt; but no driven level shifter breakouts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Convention mismatches are the hardest bugs.&lt;/strong&gt; The code was correct by its own logic. The net arrays were correct by the KiCad convention. The footprint was correct by its own convention. The bug was in the assumption that both sides used the same convention. No single piece of code was wrong — the error was in the interface between two correct pieces. This is the class of bug that code review, static analysis, and automated testing all miss, because each component passes its own tests.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Text-based PCB design cuts both ways.&lt;/strong&gt; The scriptable pipeline that let me generate and route a board in twenty minutes also let me ship a subtle pin-numbering bug to fabrication in twenty minutes. A graphical PCB editor would have forced me to visually place the header footprint and see the pin numbers on screen, which might have triggered a "wait, that doesn't look right" moment. The speed of automation is a liability when the automation is wrong. The counterargument is that graphical editors have their own class of invisible bugs — accidentally moved components, stray traces from mis-clicks, forgotten net connections. Text-based design doesn't eliminate bugs; it changes which bugs are likely.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pin numbering standards exist for a reason.&lt;/strong&gt; The IPC standard for dual-row connector numbering is zigzag. KiCad follows it. Every 2xN header footprint in every major footprint library follows it. When you write your own footprint generator, you need to follow it too. The column-first iteration (&lt;code&gt;for col... for row...&lt;/code&gt;) is a natural coding pattern — it's how you'd iterate a 2D array in most languages. It's also wrong for connector pin numbering. Convention over intuition.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The fabrication was perfect.&lt;/strong&gt; I want to emphasize this because it's easy to conflate "the board doesn't work" with "the board was made badly." &lt;a href="https://baud.rs/youwpy"&gt;PCBWay&lt;/a&gt; manufactured exactly what the Gerber files specified, with excellent quality. Every trace, via, drill hit, and solder mask opening matched the design files. The bug was in my design files, not their manufacturing process. The distinction matters: when a board comes back dead, the first question should be "is my design correct?" not "did the fab house make an error?"&lt;/p&gt;
&lt;h3&gt;The Fix in Context&lt;/h3&gt;
&lt;p&gt;This is the second failure mode for this project, and both have been instructive. The &lt;a href="https://tinycomputers.io/posts/fiverr-pcb-design-arduino-giga-shield.html"&gt;v0.1 board&lt;/a&gt; failed because the TXB0108 auto-sensing level shifters couldn't handle Z80 tri-state bus conditions — a component selection problem. The v0.3 board failed because of a pin numbering convention mismatch in the software that generates the PCB — a toolchain problem. Neither was a manufacturing defect. Both were design errors that passed every automated check and only surfaced when physical hardware was connected.&lt;/p&gt;
&lt;p&gt;The v0.4 respin will fix the pin numbering and go back to &lt;a href="https://baud.rs/youwpy"&gt;PCBWay&lt;/a&gt; for fabrication. The turnaround time from fix to new boards is probably ten days — twenty minutes for the software pipeline, a few days for PCBWay's production, and a few days for shipping. In the meantime, the v0.3 boards are useful as physical references for component placement and as evidence that the level shifters themselves work correctly (the single-row header signals all translate properly through the SN74LVC8T245s).&lt;/p&gt;
&lt;p&gt;The Python build script, pcb-rnd source files, Gerber outputs, and the test sketch are all open source:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://baud.rs/pOawfA"&gt;giga-shield&lt;/a&gt;&lt;/strong&gt; — Complete design files, build pipeline, and test firmware&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;What's Next&lt;/h3&gt;
&lt;p&gt;Part 2 of this series was supposed to cover assembled boards and Z80 bus captures. It will — just with v0.4 boards instead of v0.3. In the meantime, the v0.4 Gerbers are being generated and will be sent to PCBWay for the respin. The fix is one line. The lesson was worth more.&lt;/p&gt;</description><category>arduino</category><category>arduino giga</category><category>claude code</category><category>debugging</category><category>hardware</category><category>level shifter</category><category>open-source</category><category>pcb design</category><category>pcbway</category><category>retroshield</category><category>z80</category><guid>https://tinycomputers.io/posts/how-a-pin-numbering-bug-killed-a-pcb.html</guid><pubDate>Sat, 18 Apr 2026 15:00:00 GMT</pubDate></item><item><title>Designing a Dual Z80 RetroShield: Ground Planes, Ghost Shorts, and the Fix (Part 2)</title><link>https://tinycomputers.io/posts/designing-a-dual-z80-retroshield-part-2.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/designing-a-dual-z80-retroshield-part-2_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;29 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;Les Orchard's &lt;a href="https://baud.rs/FtBjVK"&gt;"Grief and the AI Split"&lt;/a&gt; identifies something real. AI tools have revealed a division among developers that was previously invisible, because before these tools existed, everyone followed the same workflow regardless of motivation. Now the motivations are exposed. Some developers grieve the loss of hand-crafted code as a practice with inherent value. Others see the same tools and feel relief: the tedious parts are handled, the interesting parts remain. Orchard frames this as a split between people. Craft-oriented developers on one side, results-oriented developers on the other.&lt;/p&gt;
&lt;p&gt;He's right that the split exists, and the piece clearly resonated with software creators because it names something people have been feeling but couldn't articulate. The observation is sharp. Where I think it can be extended is in where the line falls.&lt;/p&gt;
&lt;p&gt;Orchard draws the line between people. I think it falls between tasks. The same person crosses that line dozens of times a day, moving between work that demands human judgment and work that doesn't, between moments where the craft concentrates and moments where it was never present in the first place. The split is real. It's just not an identity.&lt;/p&gt;
&lt;h3&gt;The Kernel I Didn't Write&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://tinycomputers.io/posts/jokelaos-bare-metal-x86-kernel.html"&gt;JokelaOS&lt;/a&gt; is a bare-metal x86 kernel: 2,000 lines of C and NASM assembly, booting from a Multiboot header through GDT (Global Descriptor Table, which defines memory segments and access rights) and IDT (Interrupt Descriptor Table, which maps interrupt vectors to service routines) setup, paging, preemptive multitasking with Ring 3 isolation, a network stack that responds to pings, and an interactive shell. No forks. No libc. Every &lt;code&gt;memcpy&lt;/code&gt;, every &lt;code&gt;printf&lt;/code&gt;, every byte-order conversion written from scratch.&lt;/p&gt;
&lt;p&gt;I didn't write most of it. Claude did.&lt;/p&gt;
&lt;p&gt;In Orchard's framework, this should place me firmly in the "results" camp. I used AI to produce 2,000 lines of systems code; clearly I care about the outcome, not the process. But that framing misses what actually happened during the project.&lt;/p&gt;
&lt;p&gt;The decisions that made JokelaOS work were not typing decisions. They were sequencing decisions: bring up serial output first, because without it you have no diagnostics for anything that follows. Initialize the GDT before the IDT, because interrupt handlers need valid segment selectors. Get the bump allocator working before the PMM (Physical Memory Manager), because page tables need permanent allocations before you can manage dynamic ones. These choices come from understanding how x86 protected mode actually works, which subsystems depend on which, and what the failure modes look like when you get the order wrong.&lt;/p&gt;
&lt;p&gt;Claude generated the GDT setup code. I decided what the GDT entries should be, caught the access byte errors, and debugged the triple faults when segment selectors were wrong. Claude wrote the process scheduler. I determined that the TSS (Task State Segment, which tells the CPU where to find the kernel stack when switching privilege levels) needed updating on every context switch and diagnosed the General Protection Faults that occurred when it wasn't. Claude produced the RTL8139 network driver. I decided to bring up ARP before ICMP, caught a byte-order bug in the IP checksum, and validated that the packets leaving QEMU were actually well-formed.&lt;/p&gt;
&lt;p&gt;The typing was delegated. The architecture, the sequencing, the diagnosis, the validation: those were mine. If you asked me whether JokelaOS involved craft, I would say yes, more than most projects I've done. If you asked me where the craft was, I would not point at any line of code.&lt;/p&gt;
&lt;h3&gt;The Board That Failed Twice&lt;/h3&gt;
&lt;p&gt;The &lt;a href="https://tinycomputers.io/posts/fiverr-pcb-design-arduino-giga-shield.html"&gt;Giga Shield&lt;/a&gt; tells a longer version of the same story, and it's messier, because hardware involves the physical world in a way that software doesn't.&lt;/p&gt;
&lt;p&gt;The project started with a $468 Fiverr commission. I gave a designer in Kenya the spec documents, the components I thought should be used, and the form factor requirements: an &lt;a href="https://baud.rs/poSQeo"&gt;Arduino Giga R1&lt;/a&gt; shield with bidirectional level shifters, 72 channels of 3.3V-to-5V translation, KiCad deliverables. He produced a clean design. Nine &lt;a href="https://baud.rs/y9JJt9"&gt;TXB0108PW&lt;/a&gt; auto-sensing translators on a two-layer board. &lt;a href="https://baud.rs/youwpy"&gt;PCBWay&lt;/a&gt; fabricated it. Professional work, quick turnaround, and &lt;a href="https://baud.rs/youwpy"&gt;PCBWay&lt;/a&gt; sponsored the fabrication.&lt;/p&gt;
&lt;p&gt;Then I plugged in the &lt;a href="https://baud.rs/87wbBL"&gt;RetroShield Z80&lt;/a&gt; and the board was blind.&lt;/p&gt;
&lt;p&gt;The TXB0108 detects signal direction automatically by sensing which side is driving. For most applications, that's a feature. For a Z80 bus interface, it's fatal. During bus cycles, the Z80 tri-states its address and data lines. The pins go high-impedance: not high, not low, floating. The TXB0108 can't determine direction from a floating signal. It guesses wrong, and the Arduino reads garbage. I'd paid $468 for a board that couldn't see half of what the processor was doing.&lt;/p&gt;
&lt;p&gt;Nobody caught this in the design phase. Not the Fiverr designer, who was working from the spec I gave him. Not me, when I reviewed the schematic. The TXB0108 datasheet doesn't scream "incompatible with tri-state buses"; you have to understand what tri-stating means in practice and recognize that auto-sensing can't handle it. That understanding came from plugging the board in and watching it fail.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://tinycomputers.io/posts/redesigning-a-pcb-with-claude-code-and-open-source-eda-part-1.html"&gt;redesign&lt;/a&gt; used Claude to replace all nine auto-sensing translators with &lt;a href="https://baud.rs/zQqo34"&gt;SN74LVC8T245&lt;/a&gt; driven level shifters. Driven shifters have an explicit direction pin: you tell them which way to translate, and they do it regardless of whether the signal is being actively driven. Claude wrote Python scripts that pulled apart the KiCad schematic files, extracted all 72 signal mappings across 9 ICs, and generated new board files with the correct components and pin assignments.&lt;/p&gt;
&lt;p&gt;I was about to submit the revised design to PCBWay when I realized we needed a tenth level shifter. The original nine covered not just the digital pins that map to the Z80 RetroShield but all of the analog pins on the Giga, giving complete 3.3V-to-5V coverage across the board. But with driven shifters, each IC has a single direction pin controlling all eight channels. Signals that need to travel in opposite directions at different times can't share an IC without creating bus contention. Some of the channel assignments had conflicting direction requirements, and the only fix was a tenth IC to separate them.&lt;/p&gt;
&lt;p&gt;Adding one more TSSOP-24 package to an already dense two-layer board broke the trace routing. The board that had been routable with nine ICs was unroutable with ten. Moving to four layers helped but still left two to four traces with no viable path. The solution was a six-layer stackup, which needed a copper pour layer to act as a common ground plane. The open-source autorouter Freerouting couldn't handle a full copper pour; its architecture has no concept of flood-fill connectivity. So I used &lt;a href="https://baud.rs/wdr0dP"&gt;Quilter.ai&lt;/a&gt;, an AI trace router, to route the six-layer board with the ground plane that the open-source tooling couldn't represent.&lt;/p&gt;
&lt;p&gt;Count the layers of delegation and intervention in this project. I delegated the initial design to a human professional. Physics revealed the flaw. I delegated the redesign to an AI. I caught the missing tenth shifter before it went to fabrication. I delegated the trace routing to another AI. PCBWay is currently manufacturing these boards. At every stage, the work alternated between labor that could be delegated and judgment that couldn't. The Fiverr designer did skilled labor. Claude did skilled labor. Quilter.ai did skilled labor. The craft was never in the labor. It was in knowing when the labor was wrong.&lt;/p&gt;
&lt;h3&gt;Where the Craft Actually Lives&lt;/h3&gt;
&lt;p&gt;Both of these projects point at the same thing. The craft isn't in the typing, the routing, or the code generation. It's in a layer that sits above and around all of that: the judgment layer.&lt;/p&gt;
&lt;p&gt;The judgment layer is where you decide what to build next. Where you recognize that the output is wrong before you can articulate why. Where you sequence subsystems based on dependency chains that aren't documented anywhere. Where you plug a board in and notice that the readings don't make sense. Where you catch a missing component that the AI, the designer, and the autorouter all missed because none of them were thinking about the problem at that level.&lt;/p&gt;
&lt;p&gt;This layer has specific properties. It requires contact with the problem domain, not just the code or the schematic but the actual behavior of the system under real conditions. It depends on accumulated experience: understanding what tri-stating means in practice, knowing that x86 protected mode has forty years of backward-compatible traps waiting for you. And it's the part that AI is worst at, precisely because it requires grounding in physical or logical reality that language models don't have access to.&lt;/p&gt;
&lt;p&gt;The TXB0108 failure is the clearest example. The information needed to predict this failure existed in the datasheets. But recognizing its relevance required understanding what a Z80 bus cycle actually looks like at the electrical level, which required either experience with the hardware or a simulation environment that nobody had set up. No amount of language model capability substitutes for plugging in the board and watching it fail.&lt;/p&gt;
&lt;h3&gt;The Same Person in Both Modes&lt;/h3&gt;
&lt;p&gt;Orchard describes himself as results-oriented. He learned programming languages as "a means to an end" and gravitated toward AI tools because they let him focus on the outcome. He acknowledges that craft-oriented developers experience genuine loss. His framing is empathetic, but it still draws the line between people.&lt;/p&gt;
&lt;p&gt;The line doesn't hold, because I'm both of his archetypes depending on the hour.&lt;/p&gt;
&lt;p&gt;On Tuesday I might use Claude to generate a hundred lines of systemd service configuration because I need Ollama running on a machine and I don't care about the elegance of the unit file. On Wednesday I might spend three hours hand-debugging why &lt;code&gt;rocm-smi&lt;/code&gt; reports GPU utilization at zero percent: reading kernel logs, checking DKMS module versions, testing &lt;code&gt;HSA_OVERRIDE_GFX_VERSION&lt;/code&gt; values, loading the &lt;code&gt;amdgpu&lt;/code&gt; module manually because it didn't auto-load at boot. The first task is pure delegation. The second is pure craft. Both are mine. Both happened this week.&lt;/p&gt;
&lt;p&gt;When I wrote &lt;a href="https://tinycomputers.io/posts/the-economics-of-owning-your-own-inference.html"&gt;the economics piece&lt;/a&gt;, I used Claude to draft sections and I measured real power draw with &lt;code&gt;nvidia-smi&lt;/code&gt; and &lt;code&gt;rocm-smi&lt;/code&gt; at 500-millisecond intervals. I let AI handle the prose scaffolding and I personally caught that Ollama on the Strix Halo had been running entirely on CPU because the systemd service file was missing an environment variable. Every benchmark I'd trusted before finding that bug was wrong. No AI caught it. I caught it because the numbers felt off.&lt;/p&gt;
&lt;p&gt;These aren't different people. They're different tasks. The identity framing ("I'm a craft developer" or "I'm a results developer") obscures what's actually a task-level decision that experienced people make constantly: this piece of work benefits from my full attention; this piece doesn't.&lt;/p&gt;
&lt;h3&gt;What the Grief Is About&lt;/h3&gt;
&lt;p&gt;The craft-grief that Orchard describes is real and worth taking seriously. Part of it targets the wrong thing. Part of it doesn't.&lt;/p&gt;
&lt;p&gt;What's being mourned is typing as the bottleneck. For forty years, the primary constraint on software projects was the speed at which a human could produce correct code. Design mattered, architecture mattered, but someone still had to sit down and type it. The typing was slow enough that it forced a certain kind of attention. You couldn't write a function without thinking about it, because writing it took long enough that thinking was unavoidable. The bottleneck created the conditions for craft, and it felt like the craft itself.&lt;/p&gt;
&lt;p&gt;AI removes the bottleneck. Code appears in seconds. The thinking isn't forced by the typing anymore; it has to be deliberate. And that shift feels like a loss, because the rhythm of the work has changed. The long, meditative stretches of writing code, where your understanding deepened as your fingers moved, are replaced by short bursts of generation followed by review. The texture is different.&lt;/p&gt;
&lt;p&gt;But the craft didn't live in the texture. It lived in the judgment that the texture incidentally supported. The experienced developer who hand-writes a function isn't doing craft because the typing is slow. The typing is slow, and the craft happens during the slowness, but the craft is the decisions: what to name things, what to abstract, what edge cases to handle, when to stop. Those decisions haven't gotten easier. If anything, they've gotten harder, because AI lets you attempt projects that would have been too large to type by hand, which means you hit the judgment bottleneck more often and at higher stakes.&lt;/p&gt;
&lt;p&gt;JokelaOS would have taken me months to type by hand. I probably wouldn't have attempted it. With AI handling the code generation, I attempted it in days and spent the entire time making architecture and debugging decisions. The project had more craft in it than most things I've built, precisely because the typing wasn't the bottleneck. The judgment was.&lt;/p&gt;
&lt;h3&gt;The Biological Ceiling&lt;/h3&gt;
&lt;p&gt;I wrote in &lt;a href="https://tinycomputers.io/posts/the-ai-vampire-is-jevons-paradox.html"&gt;the AI Vampire piece&lt;/a&gt; that human judgment is the binding constraint in a Jevons cycle operating on cognitive output. AI makes the labor cheaper; demand expands; the expansion concentrates on the one input that can't scale: human attention and judgment. The three-to-four-hour ceiling on deep work is biological, not cultural, and no amount of productivity tooling changes it.&lt;/p&gt;
&lt;p&gt;The task-level split is where this plays out in practice. AI compresses the labor side of every project: the code generation, the trace routing, the prose drafting, the schematic extraction. What remains is denser, harder, and more consequential. Every hour of work has a higher ratio of judgment to labor than it did before AI. That's why Yegge's developers feel burned out, not because they're working more hours, but because every hour is now a judgment hour.&lt;/p&gt;
&lt;p&gt;The craft isn't disappearing. It's being compressed into a smaller, denser layer. The typing is gone. The design reviews are shorter. The code appears instantly. What's left is the part that was always the actual craft: deciding what to build, recognizing when it's wrong, knowing what to test, catching the missing tenth level shifter. That layer is entirely human, it's harder than it used to be because the projects are bigger, and it's the only part that matters.&lt;/p&gt;
&lt;p&gt;Orchard identified the split correctly. The grief is real, the division is real, and the piece resonated because it named something that software creators recognized immediately. The refinement I'd offer is that the line doesn't separate two kinds of people; it separates two kinds of tasks. The craft was never in the code. It was in the decisions that surrounded the code. Those decisions haven't gone anywhere. They've just lost the slow, meditative typing that used to accompany them. What remains is craft at higher concentration, with no filler.&lt;/p&gt;
&lt;p&gt;There was something cathartic about the old way. The hours of typing weren't just production; they were a complete experience. You conceived the idea, worked through the logic, typed every character, fought the compiler, and watched it run. The whole arc from intention to execution passed through your hands. That totality had a satisfaction to it that reviewing AI-generated output doesn't replicate, even when the output is correct.&lt;/p&gt;
&lt;p&gt;And there was something else: the syntax was a sacred tongue. Not everyone could read it. Not everyone could write it. The curly braces, the pointer arithmetic, the register mnemonics formed a language that belonged to the people who had invested years learning to speak it. That exclusivity wasn't gatekeeping for its own sake; it was the mark of hard-won fluency, and it meant something to the people who had it. Now anyone can describe what they want in English and get working code back. The priesthood dissolved overnight.&lt;/p&gt;
&lt;p&gt;I feel that loss. I still create. I still orchestrate. I still catch the errors that the tools miss. But I no longer speak a language that most people can't. The judgment layer is real, and it's where the work that matters happens. But it doesn't carry the same weight as mastery of a difficult notation. Orchestrating a process is not the same as performing it, even if the orchestration requires more skill.&lt;/p&gt;
&lt;p&gt;The grief is real. It's not about the wrong thing. It's about something that actually disappeared.&lt;/p&gt;</description><category>ai</category><category>claude</category><category>craft</category><category>hardware</category><category>jevons paradox</category><category>jokelaos</category><category>judgment</category><category>pcb design</category><category>philosophy</category><category>software development</category><guid>https://tinycomputers.io/posts/the-split-isnt-between-people-its-between-tasks.html</guid><pubDate>Thu, 19 Mar 2026 13:00:00 GMT</pubDate></item><item><title>The Mathematics of PCB Trace Routing</title><link>https://tinycomputers.io/posts/the-mathematics-of-pcb-trace-routing.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/the-mathematics-of-pcb-trace-routing_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;24 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Every PCB design eventually arrives at the same moment. Components are placed. Nets are defined. The ratsnest of thin lines connecting pad to pad looks like a plate of spaghetti dropped on a cutting board. Now someone, or something, has to turn that mess into real copper traces that don't cross, don't short, and fit within the design rules. That's the routing problem.&lt;/p&gt;
&lt;p&gt;For hobbyists and professionals alike, autorouters do this work. You press a button, wait, and traces appear. But what actually happens during that wait? The answer turns out to involve some of the most elegant mathematics in computer science, and some surprisingly hard geometric constraints that no algorithm can finesse.&lt;/p&gt;
&lt;p&gt;I've been using &lt;a href="https://baud.rs/bdZw62"&gt;Freerouting&lt;/a&gt;, the open-source Specctra autorouter, for two PCB projects now: a &lt;a href="https://tinycomputers.io/posts/designing-a-dual-z80-retroshield-part-1.html"&gt;dual Z80 RetroShield&lt;/a&gt; and a &lt;a href="https://tinycomputers.io/posts/redesigning-a-pcb-with-claude-code-and-open-source-eda-part-1.html"&gt;level-shifter shield for the Arduino Giga R1&lt;/a&gt;. The second project pushed Freerouting to its limits in ways that forced me to understand how it works internally. This is what I found when I read the source code.&lt;/p&gt;
&lt;h3&gt;Not a Grid, Not a Maze&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/giga-shield/giga_shield_freerouted_top.png" alt="Freerouting result on the Giga Shield: 2-layer board with 45-degree trace routing between TSSOP-24 ICs and pin headers, rendered in pcb-rnd photo mode" style="float: right; max-width: 420px; margin: 0 0 1em 1.5em; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);"&gt;
&lt;em&gt;Giga Shield routed by Freerouting in 45-degree mode. Top layer, rendered in pcb-rnd.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Most descriptions of PCB autorouting start with &lt;a href="https://baud.rs/pblLmT"&gt;Lee's maze algorithm&lt;/a&gt; from 1961. Place the board on a grid. Flood-fill from the source pad. When the wave hits the destination, backtrack along the shortest path. It's intuitive, easy to implement, and used in introductory EDA courses everywhere.&lt;/p&gt;
&lt;p&gt;Freerouting doesn't do this.&lt;/p&gt;
&lt;p&gt;Instead of discretizing the board into a grid of cells, Freerouting operates on a continuous geometric plane. The routing space is partitioned into convex polygonal regions called expansion rooms. Each room is a chunk of free space on one layer of the board, bounded by the edges of existing obstacles (traces, vias, pads) plus their clearance halos. The rooms aren't precomputed. They're generated lazily during the search, grown on demand as the router explores new areas.&lt;/p&gt;
&lt;p&gt;This is a shape-based router, sometimes called a free-space router. The distinction matters. A grid-based router's resolution is fixed: if your grid is 0.1mm, you can't route a trace at 0.05mm offset from an obstacle, even if the design rules would allow it. A shape-based router has no such limitation. It works with exact geometry (integer-valued coordinates for precision), and the routing channels it discovers are as wide or narrow as the physical clearances actually allow.&lt;/p&gt;
&lt;p&gt;Three geometry modes control the shape of the rooms:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Room Shape&lt;/th&gt;
&lt;th&gt;Allowed Trace Angles&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;90-degree&lt;/td&gt;
&lt;td&gt;Axis-aligned rectangles&lt;/td&gt;
&lt;td&gt;Horizontal, vertical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;45-degree&lt;/td&gt;
&lt;td&gt;Octagons&lt;/td&gt;
&lt;td&gt;Plus 45-degree diagonals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Any-angle&lt;/td&gt;
&lt;td&gt;General convex polygons&lt;/td&gt;
&lt;td&gt;Unrestricted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The choice affects both routing quality and performance. Axis-aligned rectangles are fastest to compute and intersect. Octagons allow the 45-degree traces common in modern PCBs. General polygons give the router maximum freedom but at a computational cost.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/giga-shield/giga_shield_freerouted_anyangle.png" alt="Freerouting any-angle mode: traces radiate from pads at arbitrary angles rather than snapping to a 45-degree grid, showing the difference between shape-based and grid-based routing" style="float: left; max-width: 420px; margin: 0 1.5em 1em 0; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);"&gt;
&lt;em&gt;Same board in any-angle mode. Traces follow direct paths instead of 45-degree snapping.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The image to the left shows the same board routed in any-angle mode. Notice how traces leave pads at arbitrary angles, following straight-line paths toward their destinations rather than snapping to a 45-degree grid. Compare this with the image above, which used the standard 45-degree octagon mode. The any-angle result has shorter total trace length but can be harder to manufacture cleanly at tight tolerances.&lt;/p&gt;
&lt;h3&gt;The A* Core&lt;/h3&gt;
&lt;p&gt;At its heart, Freerouting's search algorithm is A*, the same algorithm that drives pathfinding in video games, robot navigation, GPS routing, and network packet delivery. A* was published by Peter Hart, Nils Nilsson, and Bertram Raphael at the &lt;a href="https://baud.rs/pqg9oG"&gt;Stanford Research Institute&lt;/a&gt; in 1968. Nearly sixty years later, it remains the standard algorithm for finding &lt;a href="https://baud.rs/XEVv2I"&gt;shortest paths in weighted graphs&lt;/a&gt; where a heuristic estimate of remaining distance is available.&lt;/p&gt;
&lt;p&gt;The mathematical foundation is straightforward. A* maintains a priority queue of candidate states, each with a cost value:&lt;/p&gt;
&lt;p&gt;$$f(n) = g(n) + h(n)$$&lt;/p&gt;
&lt;p&gt;Where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;g(n)&lt;/code&gt; is the actual accumulated cost from the start to state &lt;em&gt;n&lt;/em&gt;. In PCB routing, this includes trace length, layer changes (vias), preferred-direction penalties, and any ripped-up obstacle costs.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;h(n)&lt;/code&gt; is a heuristic estimate of the remaining cost from &lt;em&gt;n&lt;/em&gt; to the destination. This must be admissible: it must never overestimate the true remaining cost.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;f(n)&lt;/code&gt; is the total estimated cost of the best path through &lt;em&gt;n&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At each step, A* pops the state with the lowest f(n) from the queue, expands its neighbors, and adds them back with updated costs. When the destination is popped, the algorithm has found the optimal path (given an admissible heuristic).&lt;/p&gt;
&lt;p&gt;The key insight is the heuristic. Without it, A* degenerates into &lt;a href="https://baud.rs/XEVv2I"&gt;Dijkstra's algorithm&lt;/a&gt;, which explores in all directions equally. A good heuristic focuses the search toward the destination. In Freerouting's case, &lt;code&gt;DestinationDistance.calculate()&lt;/code&gt; estimates the minimum cost to reach the target, accounting for both planar distance and any required layer transitions. The sorting value in the priority queue is computed as:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sorting_value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;expansion_value&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;destination_distance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shape_entry_middle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;layer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Where &lt;code&gt;expansion_value&lt;/code&gt; is the g(n) accumulated cost, and the distance calculation is h(n). This is textbook A*.&lt;/p&gt;
&lt;h4&gt;Why A* Works So Well&lt;/h4&gt;
&lt;p&gt;A* has a remarkable optimality guarantee. If the heuristic h(n) is admissible (never overestimates), A* is guaranteed to find the shortest path. If h(n) is also consistent (satisfying the triangle inequality: h(n) &amp;lt;= cost(n, n') + h(n') for every neighbor n'), then A* never needs to re-expand a state it has already visited. This makes it both optimal and efficient.&lt;/p&gt;
&lt;p&gt;For PCB routing, the Euclidean distance between the current position and the destination pad is a natural admissible heuristic: a straight line is always shorter than any actual route that must navigate around obstacles. Freerouting's heuristic is somewhat more sophisticated, incorporating via costs for layer transitions, but the principle is the same.&lt;/p&gt;
&lt;p&gt;The efficiency gain over brute-force search is dramatic. &lt;a href="https://baud.rs/XEVv2I"&gt;Dijkstra's algorithm&lt;/a&gt; (A* with h(n) = 0) explores states in concentric rings outward from the source. On a board with N searchable regions, it visits O(N) states. A* with a good heuristic carves a narrow corridor from source to destination, visiting far fewer states. In practice, on a moderately complex board, this is the difference between milliseconds and minutes per connection.&lt;/p&gt;
&lt;h4&gt;A* Is Everywhere&lt;/h4&gt;
&lt;p&gt;The same algorithm, with different cost functions and heuristics, solves an astonishing range of problems:&lt;/p&gt;
&lt;p&gt;Game pathfinding. Every real-time strategy game since the 1990s uses A* to move units around obstacles. The grid cells are the states, movement cost is g(n), and Manhattan or Euclidean distance to the target is h(n).&lt;/p&gt;
&lt;p&gt;GPS navigation. Road networks are weighted graphs. Edge weights are travel times. A* with geographic distance as the heuristic finds near-optimal routes across millions of road segments.&lt;/p&gt;
&lt;p&gt;Robot motion planning. A robot's configuration space (position, orientation, joint angles) is the state space. A* finds collision-free paths from one configuration to another.&lt;/p&gt;
&lt;p&gt;Natural language processing. Viterbi decoding, which finds the most likely sequence of hidden states in a Hidden Markov Model, is structurally similar to A* over a trellis graph.&lt;/p&gt;
&lt;p&gt;Puzzle solving. The 15-puzzle, &lt;a href="https://baud.rs/sKzgs4"&gt;Rubik's Cube&lt;/a&gt;, Sokoban. A* with an appropriate heuristic solves them all optimally, and the heuristic is what makes the search tractable rather than exponential.&lt;/p&gt;
&lt;p&gt;What makes A* general is the abstraction. It doesn't care whether the "states" are grid squares, road intersections, robot poses, or polygonal rooms on a PCB layer. It only needs a cost function, a heuristic, and a neighbor-expansion rule. Freerouting provides all three, with the unusual twist that its states are dynamically-computed convex polygons rather than fixed graph nodes.&lt;/p&gt;
&lt;h3&gt;But A* Only Routes One Net&lt;/h3&gt;
&lt;p&gt;Here's the catch. A* finds the optimal path for a single source-destination pair. A PCB has hundreds of nets, all competing for the same physical space. Route net A first, and it might block the optimal path for net B. Route net B first, and net A suffers instead. The quality of the overall routing depends heavily on the order in which nets are processed.&lt;/p&gt;
&lt;p&gt;Freerouting handles this with rip-up-and-reroute, a strategy from the 1970s that remains the standard approach. The idea is simple: route all nets in some initial order. When a net fails (no path exists without violating design rules), rip up one or more blocking traces and add them to a retry queue. Then try again with different priorities.&lt;/p&gt;
&lt;p&gt;The implementation in &lt;code&gt;BatchAutorouter.java&lt;/code&gt; runs multiple passes over the board. On each pass, every unrouted connection is attempted. The critical detail is how ripup decisions are made. Each existing trace has a ripup cost, and the cost increases linearly with the pass number:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;ripup_cost&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;start_ripup_costs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;passNumber&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Early passes are conservative: the router avoids tearing up existing routes. Later passes become progressively more aggressive, willing to rip up more traces to find solutions. This is a controlled escalation that prevents the router from thrashing (endlessly ripping and re-routing the same nets) while still allowing it to escape local minima.&lt;/p&gt;
&lt;p&gt;The scheduler also implements a limited form of backtracking. Every few passes, the router checks whether the board score (total unrouted connections, via count, trace length) has improved. If not, it restores a previously saved board snapshot and continues from that earlier state. This is a coarse approximation of simulated annealing: occasionally accepting a worse intermediate state to explore a different region of the solution space.&lt;/p&gt;
&lt;h4&gt;Net Ordering: The Hidden Variable&lt;/h4&gt;
&lt;p&gt;The order in which nets are routed has an outsized effect on the result. By default, Freerouting routes nets in the order they appear in the DSN file, which is typically the order they were defined in the schematic. There's no sorting by airline length, fan-out degree, or criticality. The router's source code contains a commented-out sort-by-distance that was disabled in v2.3 because it "negatively impacts convergence."&lt;/p&gt;
&lt;p&gt;This means the same board can produce different routing results depending on how the DSN file was generated. I exploited this during the Giga Shield project by writing a script (&lt;code&gt;shuffle_dsn.py&lt;/code&gt;) that generates dozens of copies of the same DSN file with randomized net ordering:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_copies&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;shuffled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;31337&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shuffle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shuffled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# write shuffled DSN...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Each copy routes nets in a different sequence, converging to a different local optimum. Running 128 parallel Freerouting instances across three machines (a local Mac, a 64-core server, and a 32-core workstation) explored 128 different regions of the solution space simultaneously. The best result was measurably better than any single run. This is an embarrassingly parallel optimization: each job is independent, and you keep the best answer.&lt;/p&gt;
&lt;p&gt;The takeaway: if your autorouter isn't finding a clean solution, the problem might not be the algorithm. It might be the ordering. Changing the input order is cheaper than changing the router.&lt;/p&gt;
&lt;h3&gt;The Optimization Phase&lt;/h3&gt;
&lt;p&gt;After the initial routing passes, Freerouting enters an optimization phase controlled by the &lt;code&gt;-mp&lt;/code&gt; flag. This phase iterates over every existing via and trace in the design, processing them in a left-to-right spatial scan.&lt;/p&gt;
&lt;p&gt;For each item, the optimizer:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Rips up the item's entire connection (all traces and vias for that net segment)&lt;/li&gt;
&lt;li&gt;Re-runs up to 6 passes of the A*-based autorouter on just that connection&lt;/li&gt;
&lt;li&gt;Accepts the result only if it reduces via count or total trace length&lt;/li&gt;
&lt;li&gt;Restores the previous state if the re-route was no better&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Vias are visited before traces, reflecting the priority of via reduction. Each unnecessary via adds manufacturing cost, signal integrity degradation, and parasitic capacitance. The optimizer also alternates between preferred and non-preferred trace directions on successive passes, preventing the solution from getting stuck in a directional rut.&lt;/p&gt;
&lt;p&gt;Via positions themselves are fine-tuned by a separate algorithm (&lt;code&gt;OptViaAlgo&lt;/code&gt;). For vias connecting exactly two traces, the optimizer searches for the position that minimizes the combined weighted trace length on both layers, iteratively nudging the via toward the geometric optimum.&lt;/p&gt;
&lt;p&gt;The result of the optimization phase is typically a 15-30% reduction in via count and a 10-20% reduction in total trace length compared to the initial routing. On the Giga Shield, 60 optimization passes ran for about 45 minutes and brought the via count from ~220 down to ~158.&lt;/p&gt;
&lt;h3&gt;Why Freerouting Can't Do Copper Pours&lt;/h3&gt;
&lt;p&gt;This is where the elegance of the algorithm runs headfirst into a hard architectural limit.&lt;/p&gt;
&lt;p&gt;Every non-trivial PCB has a ground net that connects to dozens or hundreds of pads. The standard solution in commercial EDA tools is a copper pour: a filled polygon that covers an entire layer (or most of it), with clearance cutouts around non-ground features and thermal relief connections to ground pads. You don't route GND with traces. You flood-fill it.&lt;/p&gt;
&lt;p&gt;Freerouting cannot do this.&lt;/p&gt;
&lt;p&gt;The limitation isn't a missing feature that could be added with a few hundred lines of code. It's structural. Freerouting's entire architecture is built around point-to-point trace routing. The maze search, the rip-up scheduler, the optimizer: they all operate on individual connections between pairs of pads. A copper pour is a fundamentally different object. It's not a path from A to B. It's a region that grows to fill available space, adapting its shape around every obstacle on the layer.&lt;/p&gt;
&lt;p&gt;In the source code, copper pours are represented as &lt;code&gt;ConductionArea&lt;/code&gt; objects with a fixed shape set at import time. When the autorouter encounters a net that already has a &lt;code&gt;ConductionArea&lt;/code&gt;, it simply returns &lt;code&gt;CONNECTED_TO_PLANE&lt;/code&gt; and considers the job done. There's no flood-fill algorithm. There's no thermal relief generation. The router expects that the EDA tool (KiCad, pcb-rnd, etc.) has already computed the pour geometry before the DSN file was exported.&lt;/p&gt;
&lt;p&gt;For foreign nets (anything that isn't the pour's net), the &lt;code&gt;ConductionArea&lt;/code&gt; is treated as a hard obstacle. Traces can't cross it. Vias can't be placed inside it. The router routes around it as if it were a solid wall. This is exactly right from a clearance perspective, but it means the router has no ability to create, modify, or extend a pour during the routing process.&lt;/p&gt;
&lt;p&gt;The practical impact is severe for boards with fine-pitch surface-mount parts. On the Giga Shield, each &lt;a href="https://baud.rs/zQqo34"&gt;SN74LVC8T245PW&lt;/a&gt; (TSSOP-24) has three GND pins at 0.65mm pitch. The gap between adjacent pads is roughly 0.25mm. A via needs approximately 0.9mm of space (drill diameter plus annular ring plus clearance). There is physically no room to place a via next to a TSSOP-24 GND pad and connect it to a GND trace on another layer. The router can see the GND pad, it can see that it needs to be connected to other GND pads, but it cannot find a valid path because there is no valid path using its vocabulary of traces and vias.&lt;/p&gt;
&lt;p&gt;A copper pour solves this trivially. The pad sits directly on (or thermally connects to) the pour polygon. No via needed. No trace routing needed. The connectivity is implied by physical overlap. But this is a concept that simply doesn't exist in Freerouting's model of the world.&lt;/p&gt;
&lt;p&gt;On the Giga Shield project, this limitation manifested as a hard floor of 5-6 unrouted GND connections that no amount of optimization could resolve. I threw 128 parallel instances at the problem across three machines. I tried 2-layer, 4-layer, and 6-layer board configurations. I wrote custom post-processing scripts to add GND vias and MST-based bottom-layer routing. None of it worked within DRC constraints. The geometry was simply too tight. We ended up solving it with a different tool entirely, which is a story for the &lt;a href="https://tinycomputers.io/posts/redesigning-a-pcb-with-claude-code-and-open-source-eda-part-1.html"&gt;next article in this series&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;The Shove Machine&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/giga-shield/giga_shield_freerouted_bottom.png" alt="Bottom layer of the Freerouting result: dense trace routing showing how the shove algorithm packs traces tightly between through-hole pin rows" style="float: right; max-width: 420px; margin: 0 0 1em 1.5em; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);"&gt;
&lt;em&gt;Bottom layer. The shove algorithm packs traces tightly between through-hole pin rows.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;One of Freerouting's more sophisticated subsystems is its forced insertion with shove mechanism. When the A* search finds that the optimal path for a new trace passes through space occupied by an existing trace, the router doesn't immediately give up or rip up the obstacle. Instead, it tries to push the obstacle aside.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;ForcedPadAlgo&lt;/code&gt; and &lt;code&gt;ShoveTraceAlgo&lt;/code&gt; classes implement this recursively. When a new trace needs to go where an existing trace is, the existing trace is nudged perpendicular to the new trace's path. If that nudge collides with a third trace, the third trace is nudged too, and so on, up to a configurable recursion depth (default: 20 levels for traces, 5 for vias). Only if the shove cascade exceeds this depth does the router fall back to ripping up the blocking item.&lt;/p&gt;
&lt;p&gt;This is the routing equivalent of parallel parking in a tight spot. Instead of abandoning the space, you bump the neighboring cars just enough to fit. It produces much denser routing than a pure rip-up approach, especially on boards with tight clearances and many competing nets.&lt;/p&gt;
&lt;p&gt;After every trace insertion, a pull-tight pass (&lt;code&gt;PullTightAlgo&lt;/code&gt;) smooths and shortens all traces in the affected area. This is a local optimization that removes unnecessary corners, straightens diagonal segments, and reduces total trace length. The combination of global A* search, local shove, and pull-tight smoothing produces routing quality that is competitive with commercial autorouters.&lt;/p&gt;
&lt;h3&gt;Clearance Compensation: Geometry Trick&lt;/h3&gt;
&lt;p&gt;One implementation detail worth highlighting is how Freerouting handles clearance checking. Rather than testing "does this trace violate clearance with that via?" as a separate geometric predicate, Freerouting inflates every item's shape by its clearance value when storing it in the search tree. A trace with 0.254mm clearance is stored as a shape 0.254mm wider on each side. A via with 0.127mm clearance is stored as a circle 0.127mm larger in radius.&lt;/p&gt;
&lt;p&gt;This transforms all clearance checks into simple overlap tests. If two inflated shapes overlap in the search tree, there's a clearance violation. If they don't, there isn't. No separate clearance computation is needed during routing. The free-space rooms computed by the maze search are automatically clearance-legal by construction, because they're defined as the gaps between pre-inflated obstacles.&lt;/p&gt;
&lt;p&gt;This is an instance of the &lt;a href="https://en.wikipedia.org/wiki/Minkowski_addition"&gt;Minkowski sum&lt;/a&gt; from &lt;a href="https://baud.rs/pOehEY"&gt;computational geometry&lt;/a&gt;. The inflated obstacle shape is the Minkowski sum of the original shape and a disc of radius equal to the clearance. The free space is the complement of the union of all inflated obstacles. It's mathematically clean and computationally efficient.&lt;/p&gt;
&lt;h3&gt;Strengths and Weaknesses&lt;/h3&gt;
&lt;p&gt;After reading through the source and pushing the router to its limits, here's my honest assessment.&lt;/p&gt;
&lt;p&gt;Strengths:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Gridless geometry. The shape-based approach produces routing that uses space optimally, without the artifacts of grid snapping. Traces can be placed at any position and any angle (in the selected mode), not just on grid points.&lt;/li&gt;
&lt;li&gt;Mathematically sound core. The A* search with admissible heuristic guarantees optimal single-net routing. The rip-up-and-reroute scheduler provides a practical framework for multi-net optimization. These are well-understood algorithms with decades of theoretical backing.&lt;/li&gt;
&lt;li&gt;Shove + pull-tight. The forced insertion mechanism and post-routing optimization produce dense, clean routing that competes with commercial tools for signal traces.&lt;/li&gt;
&lt;li&gt;Reproducibility. Deterministic algorithm, text-based input/output, command-line interface. Same input always produces the same output. You can script it, parallelize it, and integrate it into CI pipelines.&lt;/li&gt;
&lt;li&gt;Open source. You can read the code, modify the cost functions, change the heuristics, rebuild for different Java versions, and understand exactly what the tool is doing. That's rare in EDA.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Weaknesses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No copper pour support. The most significant limitation. Any board with a meaningful ground net requires manual post-processing or a different tool for GND connectivity. This eliminates Freerouting from the running for most production boards with fine-pitch ICs.&lt;/li&gt;
&lt;li&gt;Single-threaded core. The maze search is inherently sequential. Multi-threading exists in the codebase but only at the item level (different connections routed by different threads), not within the search itself. On modern multi-core machines, this leaves most of the CPU idle.&lt;/li&gt;
&lt;li&gt;Net ordering sensitivity. The same board produces meaningfully different results depending on input order, with no built-in intelligence about which order is likely to be best. The disabled sort-by-distance suggests the developers tried and found it counterproductive.&lt;/li&gt;
&lt;li&gt;GUI initialization in batch mode. Freerouting's Swing UI code initializes even when running headless with &lt;code&gt;-de&lt;/code&gt;/&lt;code&gt;-do&lt;/code&gt; flags. On servers without X11, this requires xvfb or a virtual framebuffer, adding deployment complexity to what should be a pure command-line tool.&lt;/li&gt;
&lt;li&gt;Version regression. Freerouting v2.1.0 produced dramatically worse results than v1.9.0 on the same board (152 unrouted vs 6). The newer version isn't always better.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;What's Next&lt;/h3&gt;
&lt;p&gt;The board you see in the images above was v0.2: nine SN74LVC8T245PW shifters, 72 channels, fully routed by Freerouting on two layers. I was ready to submit it to &lt;a href="https://baud.rs/youwpy"&gt;PCBWay&lt;/a&gt; for fabrication. Then I counted the GPIO pins one more time.&lt;/p&gt;
&lt;p&gt;The Arduino Giga R1 has 76 digital I/O pins that need level shifting, plus a handful of analog and control lines. Nine 8-channel shifters give you 72 channels. That's not enough. I was four signals short. The board needed a tenth IC, which meant reworking the layout, adding more decoupling caps, and re-routing everything. The v0.2 design that Freerouting had spent hours optimizing was going in the bin.&lt;/p&gt;
&lt;p&gt;With ten shifters instead of nine, the board got denser. The GND problem got worse. And the copper pour limitation that was already a hard floor at 5-6 unrouted connections on the 9-IC board became completely impassable on the 10-IC version. I threw 128 parallel Freerouting instances at it across three machines. I tried 2-layer, 4-layer, and 6-layer configurations. I wrote custom post-processing scripts for MST-based ground routing and copper pour stitching. None of it produced a clean board within DRC constraints.&lt;/p&gt;
&lt;p&gt;The solution came from an unexpected direction: &lt;a href="https://quilter.ai"&gt;Quilter.ai&lt;/a&gt;, an AI-powered PCB router that understands copper zones. It routed the 10-IC, 6-layer board with zero unrouted nets on the first attempt. The full story of that journey, from massively parallel Freerouting across a home lab cluster to the moment Quilter solved it in one shot, is coming in Part 2 of the &lt;a href="https://tinycomputers.io/posts/redesigning-a-pcb-with-claude-code-and-open-source-eda-part-1.html"&gt;Giga Shield redesign series&lt;/a&gt;. If the mathematics of A* is the beauty of PCB routing, the GND problem is where theory meets the physical constraints of 0.65mm-pitch IC packages, and the theory blinks first.&lt;/p&gt;
&lt;p&gt;The source code for all of this, including the board generator, the net shuffler, the parallel routing scripts, and the post-processing tools, is available in the &lt;a href="https://github.com/ajokela/giga-shield"&gt;giga-shield repository&lt;/a&gt;.&lt;/p&gt;</description><category>a-star</category><category>algorithms</category><category>autorouting</category><category>eda</category><category>freerouting</category><category>hardware</category><category>mathematics</category><category>open-source</category><category>pcb design</category><guid>https://tinycomputers.io/posts/the-mathematics-of-pcb-trace-routing.html</guid><pubDate>Sun, 15 Mar 2026 16:00:00 GMT</pubDate></item><item><title>Redesigning a PCB with Claude Code and Open-Source EDA Tools (Part 1)</title><link>https://tinycomputers.io/posts/redesigning-a-pcb-with-claude-code-and-open-source-eda-part-1.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/redesigning-a-pcb-with-claude-code-and-open-source-eda-part-1_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;20 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

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

&lt;p&gt;&lt;img id="pcb-top-img" src="https://tinycomputers.io/images/giga-shield/giga-shield-v02-top.png" alt="GigaShield v0.2 PCB top view: routed two-layer board with 9 SN74LVC8T245PW level shifters, generated with Python and autorouted with Freerouting" style="float: right; max-width: 420px; margin: 0 0 1em 1.5em; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); cursor: zoom-in;"&gt;&lt;/p&gt;
&lt;div id="img-modal" class="modal" onclick="this.style.display='none'"&gt;
&lt;span class="close" onclick="document.getElementById('img-modal').style.display='none'"&gt;×&lt;/span&gt;
&lt;img class="modal-content" id="modal-img"&gt;
&lt;div id="caption"&gt;&lt;/div&gt;
&lt;/div&gt;

&lt;script&gt;
(function() {
    var img = document.getElementById('pcb-top-img');
    var modal = document.getElementById('img-modal');
    var modalImg = document.getElementById('modal-img');
    var caption = document.getElementById('caption');
    img.onclick = function() {
        modal.style.display = 'block';
        modalImg.src = this.src;
        caption.textContent = this.alt;
    };
    document.addEventListener('keydown', function(e) {
        if (e.key === 'Escape' &amp;&amp; modal.style.display === 'block') {
            modal.style.display = 'none';
        }
    });
})();
&lt;/script&gt;

&lt;p&gt;In January, I &lt;a href="https://tinycomputers.io/posts/fiverr-pcb-design-arduino-giga-shield.html"&gt;spent $468 on Fiverr&lt;/a&gt; to have a professional design an &lt;a href="https://baud.rs/poSQeo"&gt;Arduino Giga R1&lt;/a&gt; shield with level shifters. It was a good design. Nine &lt;a href="https://baud.rs/y9JJt9"&gt;TXB0108PW&lt;/a&gt; bidirectional level translators, 72 channels of 3.3V-to-5V shifting, a clean two-layer board ready for fabrication. And then I started testing it with the &lt;a href="https://baud.rs/87wbBL"&gt;RetroShield Z80&lt;/a&gt;, and the auto-sensing level shifters fell apart.&lt;/p&gt;
&lt;p&gt;The TXB0108 is a clever chip. It detects signal direction automatically, so you don't need to tell it whether a pin is input or output. For most applications, that's a feature. For a Z80 bus interface, it's a fatal flaw. During bus cycles, the Z80 tri-states its address and data lines. The outputs go high-impedance. They're not driving high or low, they're floating. The TXB0108 can't determine drive direction from a floating signal. It guesses wrong, or it doesn't drive at all, and the Arduino on the other side sees garbage. The board was blind to half of what the Z80 was doing.&lt;/p&gt;
&lt;p&gt;The fix was clear: replace the TXB0108s with &lt;a href="https://baud.rs/zQqo34"&gt;SN74LVC8T245PW&lt;/a&gt; driven level shifters. The SN74LVC8T245 has an explicit DIR pin: you tell it which direction to translate, and it does exactly that, regardless of whether the signals are being actively driven. No guessing, no ambiguity, deterministic behavior during tri-state periods. The trade-off is that you need a direction control signal for each shifter IC, but that's a small price for reliability.&lt;/p&gt;
&lt;p&gt;What wasn't clear was how to execute the redesign. I could go back to Fiverr for another $400-500. I could spend weeks learning KiCad properly. Or I could try something that had worked surprisingly well on a &lt;a href="https://tinycomputers.io/posts/designing-a-dual-z80-retroshield-part-1.html"&gt;previous project&lt;/a&gt;: use AI and open-source command-line EDA tools to design the board from a terminal, without ever opening a graphical PCB editor.&lt;/p&gt;
&lt;p&gt;This is part one of a two-part series. This piece covers the design and toolchain: how I used &lt;a href="https://baud.rs/Z6Oq4k"&gt;Claude Code&lt;/a&gt;, the gEDA ecosystem, pcb-rnd, and &lt;a href="https://baud.rs/bdZw62"&gt;Freerouting&lt;/a&gt; to go from a failed design to production-ready Gerber files. Part two will cover the physical boards, assembly, and testing against the Z80.&lt;/p&gt;
&lt;h3&gt;The Toolchain Problem&lt;/h3&gt;
&lt;p&gt;The original Fiverr design was done in KiCad 9.0. My first instinct was to modify it directly: swap the TXB0108 footprints for SN74LVC8T245, update the pin mappings, add the DIR control header, and re-route. But there was a problem. My preferred command-line PCB tool, &lt;a href="https://baud.rs/1J64T5"&gt;pcb-rnd&lt;/a&gt;, is version 3.1.4 on Ubuntu. KiCad 9.0 uses a file format version (20241229) that pcb-rnd's &lt;code&gt;io_kicad&lt;/code&gt; plugin doesn't support. When I tried to open the KiCad PCB:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;unexpected layout version number (perhaps too new)
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Hard stop. No conversion path exists from KiCad 9.0 to pcb-rnd. The formats aren't just different versions. KiCad's S-expression format and pcb-rnd's text-based format are fundamentally different syntaxes.&lt;/p&gt;
&lt;p&gt;I could have started KiCad and used its GUI. But I'd already proven to myself with the &lt;a href="https://tinycomputers.io/posts/designing-a-dual-z80-retroshield-part-1.html"&gt;dual Z80 RetroShield project&lt;/a&gt; that text-based, AI-assisted PCB workflows are not only possible but sometimes preferable. The gEDA/pcb-rnd file format is human-readable. AI can parse it, reason about it, and generate it. A Python script can manipulate it. You can &lt;code&gt;diff&lt;/code&gt; two boards and see exactly what changed. None of that is true for a graphical-only workflow.&lt;/p&gt;
&lt;p&gt;So the plan became: extract everything useful from the KiCad source files, then rebuild the board from scratch in pcb-rnd's native format using Python. Sound insane? It kind of is. But it worked.&lt;/p&gt;
&lt;h3&gt;Extracting the DNA&lt;/h3&gt;
&lt;p&gt;Even though pcb-rnd couldn't read the KiCad files directly, the KiCad files contained all the design intelligence I needed. Component positions, net assignments, pin mappings, board dimensions. It was all there, just in a format I couldn't import.&lt;/p&gt;
&lt;p&gt;KiCad's CLI tools (&lt;code&gt;kicad-cli&lt;/code&gt;) could export what I needed:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# Component positions (X, Y, rotation for each part)&lt;/span&gt;
kicad-cli&lt;span class="w"&gt; &lt;/span&gt;pcb&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pos&lt;span class="w"&gt; &lt;/span&gt;AlexJ_bz_ArduinoGigaShield.kicad_pcb&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;giga_pos.csv

&lt;span class="c1"&gt;# Netlist connectivity&lt;/span&gt;
kicad-cli&lt;span class="w"&gt; &lt;/span&gt;pcb&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ipc2581&lt;span class="w"&gt; &lt;/span&gt;AlexJ_bz_ArduinoGigaShield.kicad_pcb&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;giga_netlist.d356
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The schematic file (&lt;code&gt;AlexJ_bz_ArduinoGigaShield.kicad_sch&lt;/code&gt;) was an S-expression text file I could parse to extract the signal mappings: which Giga pin connects to which 5V header pin through which level shifter channel. This was the most critical piece: getting the net assignments wrong would mean the board physically connects but logically doesn't work.&lt;/p&gt;
&lt;p&gt;This is where Claude Code earned its keep. I described the KiCad schematic structure and asked it to help me parse out the signal mappings. The KiCad schematic uses hierarchical sheets with positional net connections, which isn't the simplest format to work with manually, but straightforward for an AI that can read S-expressions and track net names across sheets. Within an hour, I had a complete mapping of all 72 signal channels across the 9 shifter ICs.&lt;/p&gt;
&lt;h3&gt;Generating the Board with Python&lt;/h3&gt;
&lt;p&gt;With positions and nets extracted, I wrote &lt;code&gt;build_giga_shield.py&lt;/code&gt;, a single Python script that generates the entire pcb-rnd board from scratch. No GUI involved. Every component footprint, every pin, every net connection is defined programmatically.&lt;/p&gt;
&lt;p&gt;The script is structured around four generator functions:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;tssop24_element()&lt;/code&gt;&lt;/strong&gt; generates the SN74LVC8T245PW footprint. TSSOP-24 is a precise geometry: 0.65mm pin pitch, 6.4mm pad-to-pad span, 24 pins. The function calculates pad positions mathematically: 12 pins on the left, 12 on the right, with pin 1 marked as square per convention. Getting the pin numbering right was critical. The SN74LVC8T245's datasheet shows pins 1-12 on the left (DIR, A1-A4, GND, A5-A8, OE#, GND) and pins 13-24 on the right counting bottom-to-top (B8-B5, VCCB, B4-B1, VCCA, VCCA, VCCB).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;pin_header_element()&lt;/code&gt;&lt;/strong&gt; handles through-hole pin headers with rotation support. The Arduino Giga R1 has an unusual form factor: the long pin headers run along the board edges horizontally, not vertically. In the original KiCad design, these were placed with 90-degree or -90-degree rotation. Without matching that rotation, a 26-pin header at y=84mm would extend 63.5mm downward to y=148mm, well past the 90mm board edge. The rotation transform was simple once identified:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;py&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rot&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;rot&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;py&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;smd_0603_element()&lt;/code&gt;&lt;/strong&gt; creates the 0603 footprint shared by all 27 decoupling capacitors and 9 pull-down resistors. Small SMD parts, simple geometry.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;mounting_hole_element()&lt;/code&gt;&lt;/strong&gt; places the four 3.2mm mounting holes that align with the Arduino Giga's standoff positions.&lt;/p&gt;
&lt;p&gt;The coordinate system was the trickiest part. KiCad uses an arbitrary origin; in this design, x=106mm, y=30.5mm. pcb-rnd uses (0,0). Every KiCad coordinate had to be translated:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;KX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;106.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;30.5&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;kpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ky&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kx&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;KX&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;mm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ky&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;KY&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;build_pcb()&lt;/code&gt; function ties everything together: place components, assign nets, build the symbol table, generate the layer stack, and write out a valid pcb-rnd &lt;code&gt;.pcb&lt;/code&gt; file. Running the script produces a complete, unrouted board: components placed, netlist defined, silkscreen text positioned, board outline drawn. Ready for routing.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;python3&lt;span class="w"&gt; &lt;/span&gt;build_giga_shield.py
Generated&lt;span class="w"&gt; &lt;/span&gt;giga_shield.pcb
Board:&lt;span class="w"&gt; &lt;/span&gt;155mm&lt;span class="w"&gt; &lt;/span&gt;x&lt;span class="w"&gt; &lt;/span&gt;90mm
9x&lt;span class="w"&gt; &lt;/span&gt;SN74LVC8T245PW&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;TSSOP-24&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;level&lt;span class="w"&gt; &lt;/span&gt;shifters
DIR&lt;span class="w"&gt; &lt;/span&gt;control&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;J11&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;1x10&lt;span class="w"&gt; &lt;/span&gt;header&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;The Format Wars&lt;/h3&gt;
&lt;p&gt;Getting pcb-rnd to actually accept the generated file was its own adventure. pcb-rnd's parser is strict about things that look optional in the documentation, and its error messages are sometimes misleading. An error in an Element definition might be reported as a syntax error in the Layer section fifty lines later.&lt;/p&gt;
&lt;p&gt;Three format issues bit me hardest:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The &lt;code&gt;"smd"&lt;/code&gt; flag.&lt;/strong&gt; I initially generated elements with &lt;code&gt;Element["smd" "TSSOP24" "U1" ...]&lt;/code&gt;, which seemed logical for surface-mount parts. pcb-rnd rejected it with "Unknown flag: smd ignored," which cascaded into a complete parse failure. The fix: use an empty string &lt;code&gt;Element["" "TSSOP24" "U1" ...]&lt;/code&gt;. The SMD-ness is implicit from using &lt;code&gt;Pad[]&lt;/code&gt; entries instead of &lt;code&gt;Pin[]&lt;/code&gt; entries.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bare zeros.&lt;/strong&gt; pcb-rnd is inconsistent about whether &lt;code&gt;0&lt;/code&gt; and &lt;code&gt;0nm&lt;/code&gt; are interchangeable. In some contexts, bare &lt;code&gt;0&lt;/code&gt; works fine. In others, it causes a silent parse error that manifests as a syntax error dozens of lines later. The defensive fix: always use &lt;code&gt;0nm&lt;/code&gt;, never bare &lt;code&gt;0&lt;/code&gt;, everywhere.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Missing flags on Layer lines.&lt;/strong&gt; The &lt;code&gt;Line[]&lt;/code&gt; entry inside Layer blocks needs 7 fields, not 6. The seventh is a flags string like &lt;code&gt;"clearline"&lt;/code&gt;. My generator omitted it, producing &lt;code&gt;Line[x1 y1 x2 y2 thickness clearance]&lt;/code&gt;. The parser's error message: &lt;code&gt;syntax error, unexpected ']', expecting INTEGER or STRING&lt;/code&gt;, reported at the layer definition, not at the malformed line.&lt;/p&gt;
&lt;p&gt;I found these bugs using a binary search approach, truncating the file with &lt;code&gt;head -N&lt;/code&gt; and testing each truncation point until I isolated which section introduced the failure. It's crude but effective when error reporting is unhelpful. Claude Code helped enormously here. I'd paste the error and the surrounding file content, and it would spot the structural issue faster than I could.&lt;/p&gt;
&lt;h3&gt;The pcb-rnd Ecosystem&lt;/h3&gt;
&lt;p&gt;For anyone unfamiliar with the tools involved, a brief orientation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;gEDA&lt;/strong&gt; (GNU Electronic Design Automation) is a suite of open-source tools for electronic design. The original project dates to the late 1990s and includes &lt;code&gt;gschem&lt;/code&gt; (schematic capture), &lt;code&gt;pcb&lt;/code&gt; (PCB layout), and various utilities. The file formats are text-based and human-readable, a deliberate design choice that makes them scriptable and version-control-friendly. The original &lt;code&gt;pcb&lt;/code&gt; program is now deprecated.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;pcb-rnd&lt;/strong&gt; is the actively maintained successor to gEDA's &lt;code&gt;pcb&lt;/code&gt; program. It reads and writes the same text-based PCB format, but adds modern features: more export formats, better plugin support, and critically for this project, command-line export of Gerber files, PNG renderings, and Specctra DSN files. It runs on Linux (packaged for Ubuntu) but not macOS, which is why I ran it over SSH on a remote machine throughout this project.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Freerouting&lt;/strong&gt; is a Java-based autorouter that speaks the Specctra DSN/SES interchange format. You feed it a board definition with components and nets but no traces, and it computes the copper routing, finding paths for every net while respecting design rules for trace width, clearance, and via placement. It's the open-source standard for PCB autorouting and has been used in production for decades.&lt;/p&gt;
&lt;p&gt;The workflow chains these tools together:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;build_giga_shield&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;giga_shield&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pcb&lt;/span&gt;
                            &lt;span class="err"&gt;↓&lt;/span&gt;
                    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pcb&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;rnd&lt;/span&gt; &lt;span class="n"&gt;DSN&lt;/span&gt; &lt;span class="n"&gt;export&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                            &lt;span class="err"&gt;↓&lt;/span&gt;
                     &lt;span class="n"&gt;giga_shield&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dsn&lt;/span&gt;
                            &lt;span class="err"&gt;↓&lt;/span&gt;
                   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Freerouting&lt;/span&gt; &lt;span class="n"&gt;autorouter&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                            &lt;span class="err"&gt;↓&lt;/span&gt;
                     &lt;span class="n"&gt;giga_shield&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ses&lt;/span&gt;
                            &lt;span class="err"&gt;↓&lt;/span&gt;
              &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pcb&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;rnd&lt;/span&gt; &lt;span class="n"&gt;SES&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;Gerber&lt;/span&gt; &lt;span class="n"&gt;export&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                            &lt;span class="err"&gt;↓&lt;/span&gt;
                    &lt;span class="n"&gt;Production&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Every step is a command-line operation. Every intermediate file is text. Every transformation is reproducible. Change a component position in the Python script, re-run the pipeline, get new Gerber files. This is the power of text-based EDA: the entire design is version-controlled, diffable, and automatable.&lt;/p&gt;
&lt;h3&gt;Autorouting: The Machine Does the Tedious Part&lt;/h3&gt;
&lt;p&gt;With the board generated and validated in pcb-rnd, the next step was routing: connecting all 308 nets with actual copper traces across a two-layer board. This is where Freerouting comes in.&lt;/p&gt;
&lt;p&gt;The pipeline starts with exporting the unrouted board to Specctra DSN format. pcb-rnd handles this in batch mode on the remote Linux machine:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;pcb-rnd&lt;span class="w"&gt; &lt;/span&gt;-x&lt;span class="w"&gt; &lt;/span&gt;dsn&lt;span class="w"&gt; &lt;/span&gt;giga_shield.pcb
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The DSN file contains the board geometry, component placements, pad definitions, and netlist, everything the autorouter needs to compute a routing solution. One subtlety I learned the hard way: the DSN's &lt;code&gt;(structure)&lt;/code&gt; section needs explicit &lt;code&gt;(rule)&lt;/code&gt; and &lt;code&gt;(via)&lt;/code&gt; definitions. pcb-rnd's DSN exporter puts the design rules inside the net class section, but Freerouting also expects them in the structure section. Without them, the router can see the nets but can't figure out what trace widths and via sizes are legal, and it silently fails to route most connections. A two-line addition fixed this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;(via pstk_1)
(rule
  (width 0.254)
  (clearance 0.254)
)
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Freerouting itself is a Java application with both GUI and command-line modes. On my machine, I'm running a custom build from source. The current &lt;code&gt;main&lt;/code&gt; branch had a few issues I had to fix (a missing &lt;code&gt;static&lt;/code&gt; on the main method, a null pointer on &lt;code&gt;maxThreads&lt;/code&gt; in the GUI initialization, and a Gradle build compatibility issue). The v1.9 codepath was more reliable for headless routing:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;java&lt;span class="w"&gt; &lt;/span&gt;-jar&lt;span class="w"&gt; &lt;/span&gt;freerouting-1.9.0-executable.jar&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-de&lt;span class="w"&gt; &lt;/span&gt;giga_shield.dsn&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-do&lt;span class="w"&gt; &lt;/span&gt;giga_shield.ses
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The autorouter loaded the 308-net board, ran through its passes, and produced a Specctra Session file containing 2911 wire segments and 172 vias. Every net connected. Every design rule satisfied. The routing took about 10 seconds for initial placement followed by optimization passes.&lt;/p&gt;
&lt;video controls autoplay loop muted playsinline style="max-width: 100%; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); margin: 1em 0;"&gt;
  &lt;source src="https://tinycomputers.io/images/giga-shield/routing-traces.mp4" type="video/mp4"&gt;
&lt;/source&gt;&lt;/video&gt;

&lt;p&gt;Importing the routes back into pcb-rnd was the final step. pcb-rnd can import SES files through its batch mode:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;pcb-rnd&lt;span class="w"&gt; &lt;/span&gt;--gui&lt;span class="w"&gt; &lt;/span&gt;hid_batch&lt;span class="w"&gt; &lt;/span&gt;giga_shield.pcb&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&amp;lt;EOF&lt;/span&gt;
&lt;span class="s"&gt;ImportSes(giga_shield.ses)&lt;/span&gt;
&lt;span class="s"&gt;SaveTo(LayoutAs, giga_shield_routed.pcb)&lt;/span&gt;
&lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The result: a fully routed PCB with 2911 traces and 172 vias, ready for Gerber export.&lt;/p&gt;
&lt;h3&gt;Running pcb-rnd Over SSH&lt;/h3&gt;
&lt;p&gt;One of the more unusual aspects of this project is that all pcb-rnd operations happened on a remote Ubuntu 24.04 machine accessed over SSH. pcb-rnd isn't available on macOS via Homebrew (I tried; there's a deprecated &lt;code&gt;pcb&lt;/code&gt; package but no &lt;code&gt;pcb-rnd&lt;/code&gt;), and building from source on macOS looked like a rabbit hole I didn't want to enter.&lt;/p&gt;
&lt;p&gt;The remote workflow was straightforward:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# Upload the PCB&lt;/span&gt;
scp&lt;span class="w"&gt; &lt;/span&gt;giga_shield.pcb&lt;span class="w"&gt; &lt;/span&gt;alex@10.1.1.27:/tmp/

&lt;span class="c1"&gt;# Export DSN for routing&lt;/span&gt;
ssh&lt;span class="w"&gt; &lt;/span&gt;alex@10.1.1.27&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pcb-rnd -x dsn /tmp/giga_shield.pcb"&lt;/span&gt;

&lt;span class="c1"&gt;# Import SES and export gerbers&lt;/span&gt;
ssh&lt;span class="w"&gt; &lt;/span&gt;alex@10.1.1.27&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'pcb-rnd --gui hid_batch /tmp/giga_shield.pcb &amp;lt;&amp;lt;EOF&lt;/span&gt;
&lt;span class="s1"&gt;ImportSes(/tmp/giga_shield.ses)&lt;/span&gt;
&lt;span class="s1"&gt;SaveTo(LayoutAs, /tmp/giga_shield_routed.pcb)&lt;/span&gt;
&lt;span class="s1"&gt;EOF'&lt;/span&gt;

&lt;span class="c1"&gt;# Export production files&lt;/span&gt;
ssh&lt;span class="w"&gt; &lt;/span&gt;alex@10.1.1.27&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pcb-rnd -x gerber --gerberfile /tmp/giga_shield /tmp/giga_shield_routed.pcb"&lt;/span&gt;
ssh&lt;span class="w"&gt; &lt;/span&gt;alex@10.1.1.27&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pcb-rnd -x png --dpi 600 --photo-mode --outfile /tmp/top.png /tmp/giga_shield_routed.pcb"&lt;/span&gt;

&lt;span class="c1"&gt;# Download results&lt;/span&gt;
scp&lt;span class="w"&gt; &lt;/span&gt;alex@10.1.1.27:/tmp/giga_shield.*.gbr&lt;span class="w"&gt; &lt;/span&gt;.
scp&lt;span class="w"&gt; &lt;/span&gt;alex@10.1.1.27:/tmp/top.png&lt;span class="w"&gt; &lt;/span&gt;giga_shield_top.png
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It's more keystrokes than clicking Export in a GUI. But it's scriptable, repeatable, and fits into the same terminal where Claude Code is running. When I needed to iterate (move a component, re-route, re-export) I could do it in a single pipeline without switching contexts.&lt;/p&gt;
&lt;h3&gt;Claude Code as a Hardware Design Partner&lt;/h3&gt;
&lt;p&gt;I should be explicit about what Claude Code did and didn't do in this project, because the AI angle is the part people will either find most interesting or most suspicious.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What Claude Code did:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Parsed the KiCad schematic to extract the 72-channel signal mapping across 9 level shifter ICs&lt;/li&gt;
&lt;li&gt;Wrote the initial &lt;code&gt;build_giga_shield.py&lt;/code&gt; generator script, including all four footprint generators and the net assignment logic&lt;/li&gt;
&lt;li&gt;Debugged pcb-rnd format issues by analyzing error messages and file structure&lt;/li&gt;
&lt;li&gt;Managed the remote SSH workflow: uploading files, running pcb-rnd commands, downloading results&lt;/li&gt;
&lt;li&gt;Fixed bugs in the Freerouting build (the &lt;code&gt;static main&lt;/code&gt; issue, the null &lt;code&gt;maxThreads&lt;/code&gt;, the Gradle &lt;code&gt;fileMode&lt;/code&gt; API change)&lt;/li&gt;
&lt;li&gt;Handled iterative changes: "move tinycomputers.io down by a millimeter" became an edit to the Python script, a regeneration, a re-import, and a re-export, all executed as a single flow&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What Claude Code didn't do:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Make architectural decisions. The choice to use SN74LVC8T245 over TXB0108, the DIR control header design, the decision to use pull-down resistors defaulting to A-to-B direction. Those were my decisions based on understanding the Z80 bus protocol; it is also on me for selecting the TXB0108 in the first place&lt;/li&gt;
&lt;li&gt;Verify electrical correctness. I checked the SN74LVC8T245 datasheet pin mapping myself. I verified that OE# tied to GND means always-enabled. I confirmed the 10K pull-down value was appropriate for the DIR pin&lt;/li&gt;
&lt;li&gt;Replace domain knowledge. I knew why the TXB0108 failed during tri-state periods because I understand Z80 bus cycles. Claude Code could have looked up the TXB0108 datasheet, but it couldn't have diagnosed the real-world failure mode from first principles&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The pattern that emerged was: I made design decisions, Claude Code implemented them. I said "the DIR pins need pull-down resistors to default A-to-B direction," Claude Code generated the pcb-rnd Element entries with the correct footprint, position, and net assignments. I said "export gerbers at 600 DPI with photo mode," Claude Code ran the right &lt;code&gt;pcb-rnd&lt;/code&gt; command on the remote machine.&lt;/p&gt;
&lt;p&gt;This is the same division of labor I described in the &lt;a href="https://tinycomputers.io/posts/designing-a-dual-z80-retroshield-part-1.html"&gt;dual Z80 post&lt;/a&gt;: I bring the domain knowledge, the AI handles the format translation. The text-based nature of gEDA files makes this work. If the design lived in a binary format or required mouse interactions, the AI would have been far less useful.&lt;/p&gt;
&lt;h3&gt;The New Design&lt;/h3&gt;
&lt;p&gt;Here's what the redesigned board looks like:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;v0.1 (Fiverr/TXB0108)&lt;/th&gt;
&lt;th&gt;v0.2 (Claude Code/SN74LVC8T245)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Level Shifter IC&lt;/td&gt;
&lt;td&gt;TXB0108PW (TSSOP-20)&lt;/td&gt;
&lt;td&gt;SN74LVC8T245PW (TSSOP-24)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direction Control&lt;/td&gt;
&lt;td&gt;Auto-sensing&lt;/td&gt;
&lt;td&gt;Explicit DIR pin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Channels&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shifter ICs&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decoupling Caps&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pull-down Resistors&lt;/td&gt;
&lt;td&gt;9 (OE)&lt;/td&gt;
&lt;td&gt;9 (DIR)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DIR Control Header&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;J11 (1x10)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Board Dimensions&lt;/td&gt;
&lt;td&gt;155mm x 90mm&lt;/td&gt;
&lt;td&gt;155mm x 90mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Layers&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design Tool&lt;/td&gt;
&lt;td&gt;KiCad 9.0 (GUI)&lt;/td&gt;
&lt;td&gt;Python + pcb-rnd (CLI)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design Cost&lt;/td&gt;
&lt;td&gt;$468.63&lt;/td&gt;
&lt;td&gt;$0 (open source tools)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design Time&lt;/td&gt;
&lt;td&gt;~10 days (outsourced)&lt;/td&gt;
&lt;td&gt;~2 days (with AI)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The J11 header is the key addition. It's a 1x10 pin header with 9 direction control pins (one per shifter IC) and a ground reference. Each DIR pin has a 10K pull-down resistor that defaults the direction to A-to-B (3.3V to 5V). To reverse a shifter's direction (for example, when the Arduino needs to read from the Z80's data bus) you drive the corresponding J11 pin high. The Arduino firmware manages this dynamically during bus cycles.&lt;/p&gt;
&lt;p&gt;The board carries "tinycomputers.io" and "v0.2" on the silkscreen, placed near the bottom edge. Version tracking on the physical board, a lesson learned from the Fiverr experience, where I had to pay $57 for a revision just to add version text to the silkscreen.&lt;/p&gt;
&lt;h3&gt;Generating Production Files&lt;/h3&gt;
&lt;p&gt;With the routed board in hand, the final step was generating files suitable for manufacturing. pcb-rnd handles this with command-line exporters:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# Gerber files (9 layers: top/bottom copper, mask, silk, paste, outline, drill, fab)&lt;/span&gt;
pcb-rnd&lt;span class="w"&gt; &lt;/span&gt;-x&lt;span class="w"&gt; &lt;/span&gt;gerber&lt;span class="w"&gt; &lt;/span&gt;--gerberfile&lt;span class="w"&gt; &lt;/span&gt;giga_shield&lt;span class="w"&gt; &lt;/span&gt;giga_shield_routed.pcb

&lt;span class="c1"&gt;# Photo-realistic renderings&lt;/span&gt;
pcb-rnd&lt;span class="w"&gt; &lt;/span&gt;-x&lt;span class="w"&gt; &lt;/span&gt;png&lt;span class="w"&gt; &lt;/span&gt;--dpi&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;600&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--photo-mode&lt;span class="w"&gt; &lt;/span&gt;--outfile&lt;span class="w"&gt; &lt;/span&gt;top.png&lt;span class="w"&gt; &lt;/span&gt;giga_shield_routed.pcb
pcb-rnd&lt;span class="w"&gt; &lt;/span&gt;-x&lt;span class="w"&gt; &lt;/span&gt;png&lt;span class="w"&gt; &lt;/span&gt;--dpi&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;600&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--photo-mode&lt;span class="w"&gt; &lt;/span&gt;--photo-flip-x&lt;span class="w"&gt; &lt;/span&gt;--outfile&lt;span class="w"&gt; &lt;/span&gt;bottom.png&lt;span class="w"&gt; &lt;/span&gt;giga_shield_routed.pcb
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The Gerber output includes everything a fab house needs: top and bottom copper, solder mask, silkscreen, paste stencil, board outline, and drill locations. The photo-realistic PNG renderings use pcb-rnd's built-in renderer: green solder mask, gold-plated pads, white silkscreen text. They're useful for documentation and for sanity-checking the layout before sending it to fabrication.&lt;/p&gt;
&lt;p&gt;The BOM and centroid files were generated separately from the Python script's component data. The centroid file lists every SMD component's X/Y position and rotation, which is essential if you're having the boards assembled by a service rather than hand-soldering.&lt;/p&gt;
&lt;h3&gt;What's Different About This Approach&lt;/h3&gt;
&lt;p&gt;The standard way to design a PCB in 2026 is: open KiCad or Altium, draw a schematic, assign footprints, lay out the board, route traces (manually or with the built-in autorouter), and export Gerbers. It's a visual, interactive process that works well for most people and most projects.&lt;/p&gt;
&lt;p&gt;What I did is different in a few ways that I think are worth noting, even if they're not universally applicable:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The entire design is a Python script.&lt;/strong&gt; &lt;code&gt;build_giga_shield.py&lt;/code&gt; is the single source of truth. Want to move a component? Change a coordinate in the script. Want to add a net? Add it to the dictionary. Want to change every decoupling cap from 0.1uF to 0.22uF? Change a string. Then re-run the pipeline. There's no "did I save the layout?" ambiguity, no undo history to worry about, no risk of accidentally moving something with a stray mouse click.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Every intermediate file is text.&lt;/strong&gt; The &lt;code&gt;.pcb&lt;/code&gt; file, the &lt;code&gt;.dsn&lt;/code&gt; file, the &lt;code&gt;.ses&lt;/code&gt; file. All text, all diffable, all version-controllable. When I moved a component and re-routed, I could &lt;code&gt;git diff&lt;/code&gt; the PCB file and see exactly what changed. Try that with a binary PCB format.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI can participate meaningfully.&lt;/strong&gt; Because the files are text, Claude Code could read them, modify them, and verify them. It could grep for a component reference in the PCB file, find its coordinates, suggest a new position, and make the edit. It could read the Freerouting log and diagnose why routing failed. This level of AI participation simply isn't possible with graphical-only workflows.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The workflow is reproducible.&lt;/strong&gt; I can hand someone the Python script and the Freerouting JAR and they can regenerate the entire board from scratch, on any machine with Python and Java. No KiCad version compatibility issues, no plugin dependencies, no "works on my machine" problems.&lt;/p&gt;
&lt;p&gt;The trade-off is obvious: this approach requires understanding file formats at a level that graphical tools abstract away. If pcb-rnd's parser rejects your file with a misleading error message, you need to debug the file format, not just re-click a button. It's a power-user workflow. But for someone comfortable with text editors and command lines (which describes most of the audience reading a blog called tinycomputers.io), it's a viable alternative.&lt;/p&gt;
&lt;h3&gt;What's Next&lt;/h3&gt;
&lt;p&gt;The Gerber files are ready for fabrication. In part two, I'll cover ordering the boards from &lt;a href="https://baud.rs/youwpy"&gt;PCBWay&lt;/a&gt;, sourcing the SN74LVC8T245PW and passive components, and the moment of truth: plugging the RetroShield Z80 into the new shield and seeing if the Arduino can finally see the Z80's bus cycles clearly.&lt;/p&gt;
&lt;p&gt;I'll also compare the v0.2 board side-by-side with the original Fiverr v0.1 board: the TXB0108 auto-sensing design versus the SN74LVC8T245 driven design. Same board dimensions, same connector layout, fundamentally different level-shifting approach. The comparison should be instructive for anyone choosing between auto-sensing and driven level translators for bus interfaces.&lt;/p&gt;
&lt;p&gt;The Python build script, pcb-rnd source files, Gerber outputs, and all helper scripts are open source:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://baud.rs/pOawfA"&gt;giga-shield&lt;/a&gt;&lt;/strong&gt;: Complete design files, build pipeline, and production outputs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;This is part one of a two-part series. Part two will cover fabrication, assembly, and testing.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Previous posts in this series: &lt;a href="https://tinycomputers.io/posts/fiverr-pcb-design-arduino-giga-shield.html"&gt;Fiverr PCB Design ($468)&lt;/a&gt; · &lt;a href="https://tinycomputers.io/posts/designing-a-dual-z80-retroshield-part-1.html"&gt;Dual Z80 RetroShield&lt;/a&gt; · &lt;a href="https://tinycomputers.io/posts/cpm-on-arduino-giga-r1-wifi.html"&gt;CP/M on the Giga R1&lt;/a&gt; · &lt;a href="https://tinycomputers.io/posts/zork-on-retroshield-z80-arduino-giga.html"&gt;Zork on the Giga&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</description><category>ai</category><category>arduino</category><category>arduino giga</category><category>claude code</category><category>freerouting</category><category>geda</category><category>hardware</category><category>level shifter</category><category>open-source</category><category>pcb design</category><category>pcb-rnd</category><category>retroshield</category><category>z80</category><guid>https://tinycomputers.io/posts/redesigning-a-pcb-with-claude-code-and-open-source-eda-part-1.html</guid><pubDate>Fri, 13 Mar 2026 16:00:00 GMT</pubDate></item><item><title>Designing a Dual Z80 RetroShield: Two CPUs, One Bus, Zero GUI (Part 1)</title><link>https://tinycomputers.io/posts/designing-a-dual-z80-retroshield-part-1.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/designing-a-dual-z80-retroshield-part-1_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;19 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;The critical detail is timing. When the Arduino pulls BUSRQ low, the Z80 doesn't stop immediately; it finishes its current machine cycle, which can take 3–6 clock periods depending on the instruction. Only then does it tristate its address, data, and control outputs and assert BUSAK. The &lt;code&gt;while&lt;/code&gt; loop waits for that handshake to complete. During the transition, neither CPU is driving the bus, and the Arduino must not attempt any bus operations.&lt;/p&gt;
&lt;p&gt;This is a simplified version. The full firmware in part two will handle clock generation for both CPUs, memory mapping, I/O dispatch, and the arbitration policy (round-robin, priority-based, or cooperative yield). But the handshake above is the foundation everything else builds on. It's the same protocol that made multi-Z80 S-100 systems work in the early 1980s.&lt;/p&gt;
&lt;p&gt;The hardware design is the easy part. Making two 50-year-old processors cooperate is the challenge.&lt;/p&gt;
&lt;h3&gt;Source Files&lt;/h3&gt;
&lt;p&gt;All schematics, PCB files, Gerber outputs, and helper scripts for this project are open source:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://baud.rs/i4XqDV"&gt;dual-z80&lt;/a&gt;&lt;/strong&gt;: KiCad/gEDA source files, Gerber package, Python scripts for PCB manipulation, and build log&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;This is part one of a two-part series. Part two will cover board assembly, bring-up, and dual-CPU firmware.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Previous RetroShield posts: &lt;a href="https://tinycomputers.io/posts/cpm-on-physical-retroshield-z80.html"&gt;CP/M on the RetroShield&lt;/a&gt; · &lt;a href="https://tinycomputers.io/posts/fiverr-pcb-design-arduino-giga-shield.html"&gt;Fiverr PCB Design&lt;/a&gt; · &lt;a href="https://tinycomputers.io/posts/cpm-on-arduino-giga-r1-wifi.html"&gt;CP/M on the Giga R1&lt;/a&gt; · &lt;a href="https://tinycomputers.io/posts/zork-on-retroshield-z80-arduino-giga.html"&gt;Zork on the Giga&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</description><category>arduino</category><category>dual cpu</category><category>freerouting</category><category>geda</category><category>gerber</category><category>hardware</category><category>lepton-eda</category><category>multiprocessor</category><category>pcb design</category><category>pcb-rnd</category><category>retro computing</category><category>retroshield</category><category>z80</category><guid>https://tinycomputers.io/posts/designing-a-dual-z80-retroshield-part-1.html</guid><pubDate>Fri, 06 Mar 2026 14:00:00 GMT</pubDate></item><item><title>My Experience Using Fiverr for Custom PCB Design: A $468 Arduino Giga Shield</title><link>https://tinycomputers.io/posts/fiverr-pcb-design-arduino-giga-shield.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;figure&gt;&lt;img src="https://tinycomputers.io/files/arduino-giga-shield-3d-top.png"&gt;&lt;/figure&gt; &lt;p&gt;When I decided to run vintage Z80 code on a modern &lt;a href="https://baud.rs/poSQeo"&gt;Arduino Giga R1&lt;/a&gt;, I hit an immediate roadblock: voltage incompatibility. The &lt;a href="https://baud.rs/87wbBL"&gt;RetroShield Z80&lt;/a&gt; by 8-Bit Force is a fantastic piece of hardware that lets you run a real Zilog Z80 processor on an Arduino, but it's designed for the 5V-tolerant &lt;a href="https://baud.rs/CWPoOM"&gt;Arduino Mega 2560&lt;/a&gt;. The Arduino Giga R1, with its powerful STM32H747 dual-core processor and 76 GPIO pins, operates at 3.3V logic levels and can be permanently damaged by 5V signals.&lt;/p&gt;
&lt;p&gt;The solution? A custom shield with level shifters that could translate between the Giga's 3.3V world and the Z80's 5V domain. Rather than spend weeks learning PCB design software and risking amateur mistakes, I decided to outsource the work to a professional on Fiverr. Here's what that experience was like, including the full cost breakdown and everything I received.&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/fiverr-pcb-design-arduino-giga-shield_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;30 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;h3&gt;The Project Requirements&lt;/h3&gt;
&lt;p&gt;My requirements were relatively straightforward on the surface:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Design an Arduino Giga R1-compatible shield (matching the Giga's unique form factor)&lt;/li&gt;
&lt;li&gt;Include bidirectional level shifting from 3.3V to 5V on all relevant GPIO pins&lt;/li&gt;
&lt;li&gt;Provide pass-through headers so the RetroShield Z80 could plug in on top&lt;/li&gt;
&lt;li&gt;Use KiCad for the design (my preferred EDA tool for future modifications)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The Arduino Giga R1 is essentially a larger, more powerful cousin of the Arduino Mega 2560. It shares some pin compatibility but has a different physical layout with additional headers. The shield needed to accommodate all of this while providing level shifting for approximately 70+ digital I/O lines.&lt;/p&gt;
&lt;h3&gt;Why the Arduino Giga R1?&lt;/h3&gt;
&lt;p&gt;You might wonder why I chose the Arduino Giga R1 over other options. The &lt;a href="https://baud.rs/4gVIFO"&gt;Arduino Due&lt;/a&gt;, which I mentioned in my initial message to the designer, was my original consideration. It's also 3.3V logic and has a powerful ARM processor. However, the Giga R1 offers several compelling advantages:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Processing Power&lt;/strong&gt;: The Giga R1's STM32H747 is a dual-core Cortex-M7/M4 running at 480MHz and 240MHz respectively. This dwarfs the Due's 84MHz Cortex-M3. For running Z80 code, this extra headroom means I could potentially implement cycle-accurate emulation or run multiple virtual Z80s simultaneously.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Memory&lt;/strong&gt;: The Giga R1 has 2MB of internal flash and 1MB of RAM, plus it supports external memory. The Due has 512KB flash and 96KB RAM. More memory means I can load larger Z80 programs and implement more sophisticated peripherals.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Connectivity&lt;/strong&gt;: The Giga R1 includes WiFi and Bluetooth out of the box. Imagine running a Z80 BBS that's actually accessible over the internet, or wireless file transfers to a CP/M system. The possibilities are intriguing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Camera Support&lt;/strong&gt;: The Giga R1 has a camera connector. While seemingly unrelated to Z80 computing, it opens doors for interesting projects like OCR input devices or barcode reading peripherals.&lt;/p&gt;
&lt;p&gt;The trade-off is that the Giga R1's 3.3V logic requires level shifting for any 5V hardware, hence this project. The Mega 2560's 5V tolerance made the RetroShield plug-and-play, but I felt the Giga's advantages were worth the additional complexity.&lt;/p&gt;
&lt;h3&gt;Finding a Designer on Fiverr&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://baud.rs/dbDCgR"&gt;Fiverr's&lt;/a&gt; PCB design category has hundreds of sellers ranging from hobbyists charging \$20 to professional engineers charging \$500+. After reviewing portfolios and reading reviews, I found a designer named &lt;a href="https://baud.rs/tkQg41"&gt;Elijah&lt;/a&gt; (username: ekeziah) whose work looked professional and who specifically mentioned KiCad experience. He does PCB design, CAD, and firmware work.&lt;/p&gt;
&lt;p&gt;His base gig was priced reasonably, but I knew custom work like this would require negotiation. I reached out with my requirements:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"I have a relatively straightforward project. I need an Arduino Due/Giga form factor shield that can use level shifters to go from 3.3V to 5V. I have this: [RetroShield link] which is 5V, and instead of using an Arduino Mega 2560, which is 5V tolerant, I want to use either an Arduino Due or Giga which is not tolerant of 5V."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The designer responded quickly and we began negotiating scope and pricing.&lt;/p&gt;
&lt;h3&gt;The Cost Reality Check&lt;/h3&gt;
&lt;p&gt;Let me be transparent about the costs because this is often glossed over in "I made a thing" posts:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Initial Order (January 4, 2026)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Base price: \$75&lt;/li&gt;
&lt;li&gt;Custom extras negotiated: \$200&lt;/li&gt;
&lt;li&gt;Fiverr service fees: \$108.67&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Subtotal: \$383.67&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The designer initially quoted \$175, then corrected himself saying it was a typo and meant \$275. We settled on a \$275 total for the custom work with a 7-10 day timeline. Fiverr's fees added roughly 28% on top.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Revision Order (January 20, 2026)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;After receiving the initial files, I realized I wanted to add version numbering and my website URL to the silkscreen. Since the designer hadn't included the KiCad source files in the first delivery (only Gerber files), I needed him to make the changes and regenerate everything.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Revision price negotiated: \$57&lt;/li&gt;
&lt;li&gt;Fiverr service fees: \$27.96&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Subtotal: \$84.96&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Total Project Cost: \$468.63&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Is this expensive? It depends on your perspective. A professional PCB design service might charge \$100-200 per hour, and this project involved creating a schematic from scratch, laying out a moderately complex board, and generating production files. Doing it myself would have taken 20-40 hours of learning and work. At that rate, the \$468 represents reasonable value, but it's definitely not pocket change for a hobby project.&lt;/p&gt;
&lt;h3&gt;The Design Process&lt;/h3&gt;
&lt;p&gt;Communication happened entirely through Fiverr's messaging system. Here's how the project unfolded:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;January 4-8: Requirements Gathering&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The designer studied the Arduino Giga R1 documentation and the RetroShield Z80 pinout. He asked clarifying questions about whether I needed level shifting on pins 22-53 (the additional digital pins on the Giga's side headers). I confirmed that yes, all pins needed level shifting to ensure full compatibility.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;January 9: Schematic Complete&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Elijah sent the first schematic PDF showing the circuit design. The approach was clean: nine TXB0108PW 8-bit bidirectional level shifter ICs, providing 72 channels of voltage translation. Each level shifter had proper decoupling capacitors and pull-up resistors on the output enable pins.&lt;/p&gt;
&lt;p&gt;The TXB0108 is a popular choice for this application because it's bidirectional; you don't need to specify which direction each pin will operate, making it ideal for GPIO that might be configured as either input or output.&lt;/p&gt;
&lt;p&gt;This is a key design decision worth understanding. Alternative level shifter approaches include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;74LVC245 bus transceivers&lt;/strong&gt;: These require a direction control pin, which adds complexity when GPIO pins change direction dynamically&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Simple resistor dividers&lt;/strong&gt;: Work for unidirectional high-to-low shifting, but not bidirectional&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MOSFETs with pull-ups&lt;/strong&gt;: The classic BSS138 approach works well but requires one MOSFET per channel and can be slow&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dedicated level shifter ICs&lt;/strong&gt;: The TXB0108 auto-detects direction and handles both directions at high speed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The designer's choice of TXB0108 was sound; it simplifies the design and ensures the shield will work regardless of how the software configures each GPIO pin.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;January 10: Layout and Routing&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The physical layout came together quickly. The board dimensions are 155mm x 90mm, matching the Arduino Giga R1's footprint with additional space for the level shifter circuitry. The routing was done on a two-layer board, keeping things manufacturable at low-cost PCB fabs.&lt;/p&gt;
&lt;p&gt;The designer sent progress images showing the component placement with the level shifter ICs arranged along the edges of the board, close to their respective pin headers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;January 10: First Delivery&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The initial delivery included:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Gerber files (ready for PCB manufacturing)&lt;/li&gt;
&lt;li&gt;BOM (Bill of Materials) in CSV and Excel formats&lt;/li&gt;
&lt;li&gt;3D rendered images of the board&lt;/li&gt;
&lt;li&gt;Schematic PDF&lt;/li&gt;
&lt;li&gt;Component placement (CPL) file for assembly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What was missing from this first delivery: the KiCad source files. This became important later.&lt;/p&gt;
&lt;h3&gt;The Revision Request&lt;/h3&gt;
&lt;p&gt;After examining the delivered files, I noticed two things I wanted to change:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Add "v0.1" to the board silkscreen for version tracking&lt;/li&gt;
&lt;li&gt;Add my website URL (https://tinycomputers.io/) for attribution&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These are simple text changes, but without the KiCad source files, I couldn't make them myself. The Gerber files are essentially "compiled" output; you can view them and send them to a fab, but you can't easily edit them.&lt;/p&gt;
&lt;p&gt;I reached out to the designer:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Is it possible for you to add something to the silkscreen? I would like to add 'v0.1' to the 'ARDUINO GIGA R1 SHIELD', so that line would be 'ARDUINO GIGA R1 SHIELD v0.1'. And then in a smaller font, directly to the right of that above text, I would like 'https://tinycomputers.io/'. I would add these things myself but I am not seeing any KiCAD source files, the only things that open in KiCAD are the Gerber files."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We negotiated \$57 for the revision, which also included finally receiving the full KiCad source files. The revision took about two days, with a quick back-and-forth to remove quotation marks from around the URL that the designer had initially added.&lt;/p&gt;
&lt;h3&gt;What I Received: The Complete Deliverables&lt;/h3&gt;
&lt;p&gt;The final delivery package was comprehensive. Here's everything included:&lt;/p&gt;
&lt;h4&gt;Source Files (SRC_FILES/)&lt;/h4&gt;
&lt;p&gt;The complete KiCad project including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AlexJ_bz_ArduinoGigaShield.kicad_pcb&lt;/code&gt; - PCB layout file&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AlexJ_bz_ArduinoGigaShield.kicad_sch&lt;/code&gt; - Schematic file&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AlexJ_bz_ArduinoGigaShield.kicad_pro&lt;/code&gt; - Project file&lt;/li&gt;
&lt;li&gt;Multiple backup ZIPs showing the design evolution&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Having the source files means I can make future modifications myself: adding features, fixing issues, or creating derivative designs.&lt;/p&gt;
&lt;h4&gt;Gerber Files (GERBER_FILES/)&lt;/h4&gt;
&lt;p&gt;Production-ready files for PCB manufacturing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Front and back copper layers (F_Cu.gbr, B_Cu.gbr)&lt;/li&gt;
&lt;li&gt;Solder mask layers (F_Mask.gbr, B_Mask.gbr)&lt;/li&gt;
&lt;li&gt;Silkscreen layers (F_Silkscreen.gbr, B_Silkscreen.gbr)&lt;/li&gt;
&lt;li&gt;Paste layers for SMD assembly (F_Paste.gbr, B_Paste.gbr)&lt;/li&gt;
&lt;li&gt;Board outline (Edge_Cuts.gbr)&lt;/li&gt;
&lt;li&gt;Drill files (PTH.drl, NPTH.drl)&lt;/li&gt;
&lt;li&gt;Gerber job file for fab house compatibility&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These files can be uploaded directly to JLCPCB, PCBWay, OSH Park, or any other PCB fabrication service.&lt;/p&gt;
&lt;h4&gt;Bill of Materials (BOM/)&lt;/h4&gt;
&lt;p&gt;Component lists in both CSV and Excel formats:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reference&lt;/th&gt;
&lt;th&gt;Qty&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Part Number&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;C1-C27&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;0.1uF&lt;/td&gt;
&lt;td&gt;CC0603KRX7R9BB104&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R1-R9&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;10K&lt;/td&gt;
&lt;td&gt;RC0603FR-0710KL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;U1-U9&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;TXB0108PW&lt;/td&gt;
&lt;td&gt;TXB0108PWR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;J1-J10&lt;/td&gt;
&lt;td&gt;Various&lt;/td&gt;
&lt;td&gt;Pin Headers&lt;/td&gt;
&lt;td&gt;DNP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The "DNP" (Do Not Populate) entries for connectors indicate these would typically be hand-soldered rather than machine-placed, or sourced separately.&lt;/p&gt;
&lt;h4&gt;3D Renders (IMAGES/)&lt;/h4&gt;
&lt;p&gt;Professional-looking 3D renders showing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Top view with the Arduino Giga R1 mounted&lt;/li&gt;
&lt;li&gt;Bottom view showing the routing&lt;/li&gt;
&lt;li&gt;Angled perspective view&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are great for documentation and for visualizing how the final assembly will look.&lt;/p&gt;
&lt;div style="text-align: center; margin: 30px 0;"&gt;
&lt;img src="https://tinycomputers.io/arduino-giga-shield-3d-top.png" alt="Arduino Giga Shield 3D render - top view" style="max-width: 100%; border: 1px solid #ddd; border-radius: 8px;"&gt;
&lt;p style="color: #666; font-size: 12px; margin-top: 10px;"&gt;3D render showing the shield PCB with Arduino Giga R1 mounted (top view)&lt;/p&gt;
&lt;/div&gt;

&lt;div style="text-align: center; margin: 30px 0;"&gt;
&lt;img src="https://tinycomputers.io/arduino-giga-shield-3d-bottom.jpg" alt="Arduino Giga Shield 3D render - bottom view" style="max-width: 100%; border: 1px solid #ddd; border-radius: 8px;"&gt;
&lt;p style="color: #666; font-size: 12px; margin-top: 10px;"&gt;Bottom view showing the PCB routing and through-hole connections&lt;/p&gt;
&lt;/div&gt;

&lt;h4&gt;Schematic PDF (SCH_PDF/)&lt;/h4&gt;
&lt;p&gt;A beautifully laid out schematic showing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;All nine TXB0108PW level shifters with their connections&lt;/li&gt;
&lt;li&gt;Pin mapping from Arduino Giga headers to 5V output headers&lt;/li&gt;
&lt;li&gt;Power distribution (3.3V and 5V rails)&lt;/li&gt;
&lt;li&gt;Decoupling capacitor placement&lt;/li&gt;
&lt;li&gt;Four mounting holes for secure attachment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can &lt;a href="https://tinycomputers.io/arduino-giga-shield-schematic.pdf"&gt;download the full schematic PDF here&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;Reference Assets (ASSETS/)&lt;/h4&gt;
&lt;p&gt;The designer included reference materials used during the design:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Arduino Giga R1 datasheet (2MB PDF)&lt;/li&gt;
&lt;li&gt;CAD files for the Arduino Giga R1 (ABX00063)&lt;/li&gt;
&lt;li&gt;STEP files for 3D modeling&lt;/li&gt;
&lt;li&gt;DXF file of the Giga R1 outline&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are helpful for understanding the design decisions and for future reference.&lt;/p&gt;
&lt;h4&gt;Component Placement File (CPL_FILE/)&lt;/h4&gt;
&lt;p&gt;A CSV file with X/Y coordinates and rotation for each component, useful if you're having the boards assembled by a fab house rather than hand-soldering.&lt;/p&gt;
&lt;h3&gt;The Circuit Design&lt;/h3&gt;
&lt;p&gt;Looking at the schematic, the design is elegant in its simplicity. Each TXB0108PW provides 8 channels of bidirectional level shifting. With nine of these ICs, the design provides 72 channels, more than enough for all the Arduino Giga's GPIO pins.&lt;/p&gt;
&lt;p&gt;Key design elements:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Level Shifters&lt;/strong&gt;: The TXB0108PW is an 8-bit bidirectional voltage-level translator. It automatically detects the signal direction, making it perfect for GPIO that might be configured as either input or output at runtime. The A-side connects to the 3.3V Arduino Giga pins, and the B-side connects to the 5V RetroShield pins.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Decoupling Capacitors&lt;/strong&gt;: Each level shifter has a 0.1µF ceramic capacitor on both the 3.3V (VCCA) and 5V (VCCB) power pins. This is standard practice to filter high-frequency noise and ensure stable operation. With 27 capacitors total, the power rails should be rock-solid.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Output Enable Pull-ups&lt;/strong&gt;: Each TXB0108 has a 10K pull-up resistor on the OE (Output Enable) pin, tying it to 3.3V. This ensures the level shifters are always active when the Arduino is powered.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pass-Through Headers&lt;/strong&gt;: The design includes matching pin headers on both the 3.3V and 5V sides. The Arduino Giga plugs into female headers on the bottom, while the RetroShield (or other 5V shields) can plug into male headers on top.&lt;/p&gt;
&lt;h3&gt;Lessons Learned&lt;/h3&gt;
&lt;p&gt;After going through this process, here's what I'd do differently or recommend to others:&lt;/p&gt;
&lt;h4&gt;1. Specify Source Files Upfront&lt;/h4&gt;
&lt;p&gt;Make it explicit in your initial requirements that you need the original design files (KiCad, Altium, Eagle, etc.), not just Gerber output files. This saves the cost and hassle of a revision later. Many designers consider source files an "extra" unless you ask for them.&lt;/p&gt;
&lt;h4&gt;2. Include Silkscreen Details Early&lt;/h4&gt;
&lt;p&gt;Think about what text you want on the board before the design starts. Version numbers, URLs, logos, regulatory markings: all of these are easy to add during initial design but require regenerating all files if added later.&lt;/p&gt;
&lt;h4&gt;3. Budget for Fiverr's Fees&lt;/h4&gt;
&lt;p&gt;Fiverr's service fees add roughly 25-30% to the listed price. When negotiating with a designer, account for this in your mental budget. A \$275 job becomes \$350+ after fees.&lt;/p&gt;
&lt;h4&gt;4. Communicate Frequently&lt;/h4&gt;
&lt;p&gt;Don't disappear for days at a time. Quick responses keep the project moving and help catch misunderstandings early. The designer asked good clarifying questions; make sure you answer them thoroughly.&lt;/p&gt;
&lt;h4&gt;5. Review Carefully Before Approving&lt;/h4&gt;
&lt;p&gt;Take time to review delivered files carefully. Open the Gerbers in a viewer (KiCad has a built-in Gerber viewer, or use an online tool), check the schematic for obvious errors, verify the BOM has the right components. It's much cheaper to catch issues before ordering PCBs.&lt;/p&gt;
&lt;h3&gt;Alternatives to Fiverr&lt;/h3&gt;
&lt;p&gt;Before deciding on Fiverr, I considered several alternatives:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DIY with KiCad&lt;/strong&gt;: The open-source route. KiCad is free, powerful, and has excellent documentation. However, PCB design has a steep learning curve. Understanding design rules, proper trace widths, via sizes, clearances, and manufacturing constraints takes time. For a one-off project, the learning investment didn't seem justified.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Upwork or Other Freelance Platforms&lt;/strong&gt;: Similar to Fiverr but often with higher prices and a more traditional freelancer relationship. Upwork tends to attract more experienced (and expensive) engineers. For a small project like this, Fiverr's fixed-price gig format seemed more appropriate.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Local EE Students/Engineers&lt;/strong&gt;: Universities often have engineering students looking for small projects. This can be cheaper, but finding someone and managing the relationship takes effort. You also don't have the platform protections that Fiverr offers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PCB Design Services&lt;/strong&gt;: Companies like PCBWay and JLCPCB offer design services alongside manufacturing. These can be convenient but pricing varies widely and communication can be challenging across language barriers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open Source Existing Designs&lt;/strong&gt;: Sometimes you can find an existing design that's close to what you need. I looked for Arduino Giga shields with level shifting but found nothing. The Giga R1 is relatively new and its unique form factor means fewer compatible shields exist.&lt;/p&gt;
&lt;p&gt;Fiverr won because of its accessibility, fixed pricing model, and the portfolio/review system that let me evaluate designers before committing.&lt;/p&gt;
&lt;h3&gt;Was It Worth It?&lt;/h3&gt;
&lt;p&gt;For my situation, absolutely. I have a professional-quality PCB design that I can manufacture, modify, and iterate on. The alternative was spending 20-40 hours learning PCB design from scratch and likely making beginner mistakes that could damage expensive hardware.&lt;/p&gt;
&lt;p&gt;The \$468 total is significant for a hobby project, but context matters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Arduino Giga R1 costs \$90&lt;/li&gt;
&lt;li&gt;The RetroShield Z80 costs \$65&lt;/li&gt;
&lt;li&gt;PCB manufacturing will add another \$20-50 depending on quantity&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The total investment for this Z80-on-Giga project will be around \$650-700 including the shield design. That's real money, but for a unique piece of hardware that lets me run authentic Z80 code on a modern microcontroller with WiFi, Bluetooth, and a camera interface, it feels worthwhile.&lt;/p&gt;
&lt;h3&gt;A Note on Maker Culture&lt;/h3&gt;
&lt;p&gt;I'm aware that hiring someone to design a circuit board runs counter to the ethos of Maker Culture. There's something deeply satisfying about designing, building, and debugging your own hardware, learning from mistakes, understanding every trace and component choice, and earning that sense of accomplishment that comes from true DIY.&lt;/p&gt;
&lt;p&gt;Outsourcing the design felt like a shortcut, and in some ways it was. I traded the learning experience for speed and convenience.&lt;/p&gt;
&lt;p&gt;That said, I didn't stop there. After receiving the Fiverr design, I also created an alternative version of the shield myself. I used &lt;a href="https://baud.rs/Z6Oq4k"&gt;Claude Code&lt;/a&gt; to help work through the component connections and pin mappings, and &lt;a href="https://baud.rs/XRtos4"&gt;Quilter.ai&lt;/a&gt; to handle the PCB routing, an AI-powered tool that automates trace layout while respecting design rules. The result is a second design that I understand more intimately, having been involved in every decision.&lt;/p&gt;
&lt;p&gt;Once &lt;a href="https://baud.rs/youwpy"&gt;PCBWay&lt;/a&gt; manufactures both versions, I'll post a comparison of the two approaches: the professionally designed Fiverr board versus the AI-assisted DIY version. It should be an interesting look at how modern AI tools are changing what's possible for makers who want to learn by doing but also want a safety net of intelligent assistance.&lt;/p&gt;
&lt;h3&gt;Next Steps&lt;/h3&gt;
&lt;p&gt;With the design files in hand, my next steps are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Order prototype PCBs&lt;/strong&gt; from JLCPCB or PCBWay&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Source components&lt;/strong&gt; from LCSC or DigiKey (the BOM helps here)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Assemble and test&lt;/strong&gt; the prototype&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Document any issues&lt;/strong&gt; and potentially order a v0.2 revision&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write about running Z80 code&lt;/strong&gt; on the Arduino Giga R1&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The beauty of having the KiCad source files is that if I find issues during testing, I can fix them myself and generate new Gerber files. The \$57 revision cost to get those source files has already paid for itself in peace of mind.&lt;/p&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://baud.rs/dbDCgR"&gt;Fiverr&lt;/a&gt; can be a viable option for custom PCB design, especially for projects that are well-defined and don't require extensive back-and-forth iteration. The key is finding a competent designer, communicating clearly, and budgeting realistically for both the designer's fee and Fiverr's platform fees.&lt;/p&gt;
&lt;p&gt;My Arduino Giga R1 shield project cost \$468.63 total across two orders, more than I initially hoped to spend, but less than I would have paid for my own time learning PCB design. The deliverables were comprehensive, professional, and gave me everything I need to manufacture, modify, and document the design.&lt;/p&gt;
&lt;p&gt;If you're considering using &lt;a href="https://baud.rs/dbDCgR"&gt;Fiverr&lt;/a&gt; for PCB design, go in with realistic expectations about cost and timeline, and make sure to specify exactly what deliverables you need upfront. It might not be the cheapest option, but for a one-off custom project, it can be a reasonable trade-off between time and money.&lt;/p&gt;
&lt;p&gt;Now, if you'll excuse me, I have some prototype PCBs to order and a Z80 to make talk to an STM32.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Coming Soon&lt;/strong&gt;: Thanks to an upcoming sponsorship from &lt;a href="https://baud.rs/youwpy"&gt;PCBWay&lt;/a&gt;, I'll be able to bring this design from KiCad files and Gerber renders into the physical world. Stay tuned for a follow-up post where I'll document the manufacturing process, assembly, and first power-on of the Arduino Giga R1 Level Shifter Shield. Will it work on the first try? Will the Z80 finally talk to the STM32? Check back to find out!&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;The files discussed in this post, including the schematic and 3D renders, are from the actual delivered project. The Arduino Giga R1 is a product of Arduino. The RetroShield Z80 is designed by 8-Bit Force and available on Tindie.&lt;/em&gt;&lt;/p&gt;</description><category>arduino</category><category>arduino giga</category><category>fiverr</category><category>hardware</category><category>kicad</category><category>level shifter</category><category>pcb design</category><category>retroshield</category><category>z80</category><guid>https://tinycomputers.io/posts/fiverr-pcb-design-arduino-giga-shield.html</guid><pubDate>Sat, 24 Jan 2026 20:00:00 GMT</pubDate></item></channel></rss>