LLM PCB Routing: What the Contract Engineer Taught It
Seven of the sixteen PCB routing techniques in my agent's registry, most of them learned from a contract engineer, are ones it cannot execute.
On this page
- The board
- What the LLM produced
- I could have made DRC read zero and did not
- Power and clock were the same width
- What code did far better than me
- Two engagements, and the rigorous one came back empty
- Putting the two boards side by side
- Why those 11 violations disappeared, and how I got it wrong first
- Our own brief was wrong as well
- Distilled into techniques
- Replaying it without the answer key
- What it lost, and what is unknown
- The misfires belong in the record too
- Where things stand
- Where to call a human
I handed PCB routing to an LLM. It did not go well, so I hired it out. Then I put the returned board next to the previous one, net by net, and turned the differences into a registry of routing techniques for the agent to use next time.
Sixteen techniques went in. Each one names an editor slot, and seven of them read gui_card, which is what you write when no function exists and a person has to do it by hand. The move that saved this board is one of the seven.
An LLM cannot lay out a board the way a practised human does. What USD 145 with a contract engineer bought is not routing skill but a written list of where it fails, and the list outlives the board.
The model was Fable 5, from memory. The repo only ever wrote down “Claude”, so that one is my word, not a log line.
The board
21mm diameter, round, four layers, running off a CR1632 coin cell. The main module is a Raytac AN54LV-15 (nRF54L15, internal chip antenna) with a TDK ICM-42688-P for motion. The first inner layer is solid ground, which leaves two layers of real routing room for fifteen nets.
Three terms carry the rest. A net is one line in the schematic tying a signal together, roughly a single variable in software. A via is the hole drilled to move a trace between layers, and DRC is the check that sweeps the board for anything a fab would reject. Close enough to a linter.
The board is not drawn with a mouse. Python code generates it, and the constants in the rules file look like this.
TRACK_MIN_MM = 0.127 # 5 mil
CLEARANCE_MM = 0.1 # tightened (still JLCPCB-capable) to open routing room
VIA_DRILL_MM = 0.3
VIA_DIA_MM = 0.5
Read the comment on the second line. It says, in its own words, that the clearance was tightened to open up room for routing. Half of what follows starts from that one line.
What the LLM produced
The first board came back with 143 DRC violations. The diagnostic classified about 120 of them, 84%, as having no mechanical content: 90 from one bad via template, three rules broken on each of 30 vias, 16 fine-pitch clearance rule mismatches, 18 silkscreen cosmetics, 3 plane fills. That adds to 127, not 120, because the categories double-count a handful and I never reconciled it. Sixteen are left over on the arithmetic that actually adds up, and 8 of those were the real signal: courtyard overlaps.
Bringing the via spec up to our own 0.50/0.30 rule, the one printed above, took those 90 to zero in a single change. The diagnostic named the cause in one line: the freerouting autorouter’s via template ignores the board’s via rules. The padstack it emitted left a 0.075mm annular ring against our own 0.10mm rule, and the diagnostic’s verdict was that the board was not fab-legal. It was not entering a factory.
DRC reported all 90 of them honestly, from the first run. The green light did not lie. Nobody was reading it.
That fix came at a price. Bringing the vias to spec grew a field of fatter vias that created roughly 34 new density violations, and unconnected nets went from 6 to 13. The total dropped from 143 to 64 while the verdict changed to DENSITY_LIMITED. Sixty-four was the floor reachable by tuning rules, nowhere near the 20 I had set as a target.
The number improves while the verdict gets worse. That combination is where the autorouter ended.
I could have made DRC read zero and did not
While writing the contract brief I tried one thing. Widening the global clearance from the 0.1mm in that code back to 0.16mm takes the 11 boundary violations on the handoff board to zero. It also pushed unconnected nets from 6 to 15, though the brief took that count on the previous revision, not this one.
That is passing DRC by abandoning routing. I did not use it, and the brief says so in those terms.
Watching one number, this trade reads as an improvement. Eleven became zero.
Power and clock were the same width
The quietest defect was here. Battery power VBAT, ground GND, SPI clock SPI_SCK. All three nets carried the same 0.2mm trace width on the board I handed over.
Coin-cell power drawn at the width of an SPI clock. DRC cannot catch this, because no rule was broken. On the board that came back, VBAT stepped between 0.25 and 1.0mm and GND opened up to between 0.3 and 0.7.
What code did far better than me
On exhaustively sweeping part-rotation combinations, code does not even give me a game. What worked here is a for loop, not an LLM. It ran all 2^14, 16384 of them, and pulled total net length from 128.62mm down to 124.38mm. A 3.3% reduction.
A person cannot run that search. What a person can do is know whether this board is a thing a factory will accept. Superhuman at brute force, helpless at admissibility, and that split is what the editor field ends up encoding. It is also the reason AI still needs hardware engineers.
Two engagements, and the rigorous one came back empty
The first was a properly designed experiment. An outside engineer laid the board out independently, without seeing ours. The brief was explicit: do not try to match a hidden layout.
The scoring criteria were written and sealed before the work started. Write criteria after seeing results and they bend toward the result you like, so I wrote them first and made it checkable that I had not gone back. The mechanism is a fingerprint: run the file through a hash, keep the resulting string separately, and one changed character gives a different string. Our own board file got the same treatment and was kept aside.
What came back was an out-of-scope schematic rewrite. A scope gate added the same day ruled it unscoreable.
The rigorous experiment produced no comparison at all.
The second was ordinary contract work. Placement was fixed and autorouting had reached 96%. Two asks: finish the remaining 5 nets and 11 DRC violations by hand, and point out design problems we had missed. It was an open, paid engagement. They saw our board. This is not blind and should not be called an experiment.
The one that produced results is the second. I paid KRW 200,000, about USD 145, and the engineer stays anonymous.
Putting the two boards side by side
I diffed them net by net.
| Net | Before handoff | Round 1 back | Round 2 back |
|---|---|---|---|
| IMU interrupt 2 | 26.8mm, 2 vias | 9.8mm, 0 vias | 9.8mm, 0 vias |
| IMU interrupt 1 | 25.5mm | 11.8mm | 11.8mm |
| Module reset | 39.4mm | 14.4mm | 18.3mm |
| Debug data (SWDIO) | 21.3mm, 2 vias | 14.6mm, 2 vias | 16.8mm, 0 vias |
Two columns because the two rounds disagree. Round 1 is the aggressive one, and round 2 gave some of it back: module reset went from 14.4 back up to 18.3mm. The debug net moves the other way, losing its last two vias only in round 2, which is the change that pulled it off the inner layer. The board that was ordered is round 2.
All four of these nets were already fully connected on the board I handed over. An autorouter has no reason to touch them. The human deleted them and drew them again. Hold onto that: it is the move the catalogue would later file under a human’s name.


Both are rendered from the actual board files with kicad-cli 9.0.8, same geometry, same angle.
The front side is only half the story. The real change happened on inner layer 2.
Parsing the board files and counting, the nets using this layer went from 6 to 8 and the segments from 12 to 79. Only three of the original six left, and five new ones moved in. The layer is not used less, it is used differently. Two of the three that left are debug nets.
Vias across the whole board went from 37 to 58, segments from 151 to 282. The traces got shorter while the count went up.
Why those 11 violations disappeared, and how I got it wrong first
The 11 hole_clearance violations present at handoff read zero on the board that came back. The technique document in the source repo credits the via diameter sweep, the change that took the pad from 0.5mm to 0.6mm, with the drill untouched, on every via.
I thought that was wrong. Here is what I parsed:
- The 11 violations come from 5 vias. The same vias were flagged repeatedly.
- The measured clearances are all just under the 0.25mm requirement, short by 0.0077 to 0.0483mm. The brief lists them:
0.2017 0.2018 0.2092 0.2092 0.2112
0.2245 0.2248 0.2248 0.2280 0.2300 0.2423
The offending pairs: C_SC1 pad to INT_IMU via ×2, TP5 pad to SWDIO via ×2, NRESET_MOD track to SWDCLK via ×3, VBAT track to INT_IMU via ×2, SPI_SCK/SPI_MISO track to GND via ×2.
- Drill diameter is 0.3mm on all three boards. Only the pad changed.
- Those 5 vias appear nowhere on either returned revision. Nothing sits within 0.05mm of their coordinates.
From that I concluded the pad sweep could not have done it: hole_clearance measures hole to copper, so growing the pad while the hole stays put should change nothing.
That reasoning holds for the eleven already on the board. What it does not license is the step I took next, that the credit therefore belongs to the rip-up. The brief I wrote myself explains why the pad size matters at all. Foreign copper is not held off the hole. It is held off the pad edge, by the copper-to-copper rule, so hole-to-copper is the clearance plus the annular ring:
freerouting only looks at copper-to-copper clearance (0.1mm) and never at KiCad’s hole-to-copper rule (0.25mm). The via is pad Ø0.5 with drill Ø0.3, so the annular ring is only 0.1mm. Hold copper 0.1mm off the pad edge and you get 0.2mm from the drill, which is 0.05mm short.
Take the pad to Ø0.6 with the drill at 0.3 and the annular ring becomes 0.15mm. Clearance plus ring is now 0.25mm.
But that only holds for copper the router places after the change. A bigger pad does not move copper already sitting on the board, so an existing violation at 0.2017mm stays at 0.2017mm. The sweep kills the class going forward. The eleven already there needed the copper to move as well, and both mechanisms ran in the same rework.
What my parse established is that copper at those five sites moved. A resize is not a move, so the sweep alone cannot have retired the eleven. What the absence does not tell me is who moved the copper: a deliberate rip-up and a full re-route under the new via rule leave the same trace.
I am leaving the wrong version above rather than quietly replacing it, because the shape of the error is the interesting part. I had a real measurement, drew a causal conclusion it does not support, and was one grep away from the paragraph in my own brief that explains the mechanism.
Our own brief was wrong as well
Ten of the 13 DRC issues the contractor reported on the board we sent were our fault. The battery pads sit inside a keep-out region while we asked for no traces there, which makes the request impossible to satisfy from the start. We never wrote down that via-in-pad was allowed. The other 3 came from footprint files missing from our repo.
Part of what the human “found” was a defect in our paperwork, not in our board.
Distilled into techniques
The board was already finished. There was nothing left to do for this one. The goal was singular: stop the agent repeating the same mistakes on the next prototype.
So I did not give the LLM the finished board. I gave it the table above. Per net, the difference in length, via count and layer between the board I sent and the board that came back. Looking back, that table is the part of this post that did any work.
Sweeping the one span from before-handoff to returned yielded 12 PCB routing techniques. They went into a registry the agent can read back, since grown to 16 techniques over 131 lines of YAML as later runs turned up more. Same project-shaped harness format I use everywhere else. I will not list them. Two are enough.
T2, rip up and redraw. All four nets in the table above are its result. It is the most valuable move in this post, and it is filed second, under T1: via-size-normalization, the entry whose evidence line I spent the last section wrongly trying to overturn.
T2’s editor field reads gui_card, so the best move in the file is one the agent cannot run.
It is tempting to read that as the machine admitting something about itself. It is not. The schema comment is blunt about what the field is: editors MUST name real routing_editors functions. It is a dispatch table, and gui_card is the value you get when no function exists. The other six that route there include silk-nudge, which is nudging a silkscreen label. Nothing profound is being confessed about the limits of machines; somebody just never wrote that function.
What the count does say is worth more. Seven of sixteen, nearly half, route to a card a person works, and the single highest-value technique is one of them. Two more name no editor, but for the opposite reason: abstaining and feeding placement are not edits. I counted the seats a hardware product needs and found two the harness cannot fill; this is the same shape, measured.
T12, when the boundary is unknown, do not guess, step back. The file sets a confidence floor of 0.6 while some entries score 0.5. Below the floor, triage stops instead of picking a technique and hands the call to whoever is holding the design spec.
Replaying it without the answer key
This is a test of whether the PCB routing transfers. The contract engineer’s board is the answer key. The agent starts from the engineer’s round-1 board with one fix of ours on top, already routed and DRC-clean, and has to reproduce the round-2 rework without being shown it. Then the result gets scored.
Three scoring criteria were pulled from the answer key in advance and deliberately held out of the 12 techniques. Copying what you were taught is not a test.
| Criterion | Frozen threshold | Measured | Verdict |
|---|---|---|---|
| 1. Clock trace squeezing between the chip’s pad rows | no edge gap under 0.15mm | 0.105 and 0.115mm at two pads | escalated to a human |
| 2. Debug and serial via columns encroaching | at least 1.0mm between signal via centres, in the SWD and UART via window | 1.031mm on the worst pair | passes the check |
| 3. Clock or data grazing a foreign antipad | at least 0.58mm from the via centre | zero violations | solved blind |
The agent solved 2 and 3 without seeing the answer. Do not take the pass on 2 at face value. The closest pair measures 1.031mm against a frozen threshold of 1.0. The same review derives 1.25mm as what two antipads need to avoid physically merging. The scorecard passes, the physics does not, and the remainder sits on the repo’s running list of known gaps as human GUI work.
Criterion 1 went to a human. Triage maps a pad-field squeeze to T7, and T7 is a gui_card entry, so the criterion was pointed at a technique with no editor function behind it. The results document says that mapping was in place from the start. I cannot check it. The earliest committed registry postdates scoring, and the recommendation list in it names T15, a technique mined out of the answer key at scoring. So the mapping is plausible and unverified, and the escalation was written down after.
The seven gui_card entries are a list of missing functions, not a prediction. The results document claims only that the mapping is consistent with the outcome, and on the record that is as far as it goes.
Compare the first board. Ninety violations sat in a DRC report that was open on my screen, and I found them weeks later by running a diagnostic. This time what it could not do came out with a name on it.
So: 2 of 3 blind, and one accurate escalation. One GUI work order came out of it, and I never worked it. A later run executed that card headlessly and cleared all three criteria, but it had seen the returned board, so the blind is broken and it does not count. My zero hours are not a human being efficient. They are the boundary moving once the agent saw the answer.
What it lost, and what is unknown
The contract engineer redrew the debug traces without vias and pulled them off inner layer 2 entirely. My replay could not do that. On reference coverage, a lower-is-better metric, the replay scored 100 against the human’s 92, losing by 8.
There is no control group. No run of the same agent without these techniques exists, the way the firmware benchmark had one. That is precisely the experiment that would separate cause from coincidence in “give it a comparison and it works”, and I did not run it.
The board the techniques came from and the board that proved the replay are the same board. Both are the same prototype. The repo’s own gap list records this as same-board-family overfit and states that real transfer proof is the first routing session on the next prototype.
What is established so far is only that it reproduces on the same board.
The misfires belong in the record too
A few from the commit log:
eabbf91 route.py did not run at all — SWIG ownership trap,
plus two clobber hazards
The router had not been running. I had been reading output from a script that exited early, and I have no idea how long for.
The agent asserted there was no room on a 21mm board. Measured, that was wrong.
One commit re-derived the routing rationale from scratch and found the measurement tooling had never been in the repo, which changes what the adoption decision rested on.
Where things stand
I ordered five boards in 4-layer FR-4, 0.8mm, 21mm diameter, ENIG. Nothing has arrived. This is the SpotOn board, built by the AI team of one. The first order was simply wrong: I specified a 0.5mm-pitch 14-pad LGA as the bottom terminal, a part I cannot solder at my own desk.
The ordered revision also predates the technique registry. Its first real outing has not happened yet.
Everything up to here is a record of the routing process, not evidence that the board works.
Where to call a human
Watching the DRC violation count alone will miss it. When that number falls while unconnected nets rise, that is the boundary. That is the 0.16mm trade from earlier: eleven violations to zero, and unrouted nets from six to fifteen on the revision where I ran it. Crossing that boundary cost me about USD 145.
There is also a boundary that never appears in any tally. Power and clock drawn at the same 0.2mm width. No rule was broken, so DRC stays quiet. It shows up on no dashboard.
If you are trying hardware with AI, a green DRC is not a door to the factory. The two boundaries above are how you catch what DRC will not.
Two things came out of this that the board did not. The registry, which I did not have in March. And one line in the hardware checklist, written after that first board: never ship on a false green. fab.gate_ok must be cross-checked before any money-committing order. DRC-clean is necessary, not sufficient. That rule exists because for a while I was treating the green light as the door.
If you are hiring layout out, take the deliverable in a form you can put side by side with the previous revision. Per net. That table is what the registry was built from, and without it there is nothing to feed back.
Whether that seven drops on the next prototype is the one number I am watching.
Comments
Loading comments...