KiCad PCB placement
Placement is where a board is won or lost. Routing can only connect what
placement already put within reach: a buck converter whose input cap is
30 mm from its ground pin cannot be saved by clever traces.
The usual failure is a pile: parts dropped by a packer into whatever gap
fits, a regulator 50 mm from its own inductor, and a board that passes DRC
and is unroutable. A packer-style version of this skill did exactly that on a
126-part double-sided module carrier: 0 of 76 datasheet placement rules
met. The recipe-driven placer below meets 67 of 76 on the same board, and
on an unrelated 34-part single-sided board it meets 18 of 20 where a
hand-written table met 3.
What you get:
scripts/autoplace.py — the placer. Reads the netlist, groups parts
into functional blocks with recipes, solves each block, places blocks on
the board, audits every rule, draws a floor plan, writes the board.
scripts/recipes.py — the rules: which support parts belong to which
IC (found by pin names, not designators), how close, in what order, with
the datasheet or measured reference board behind each number.
scripts/pcblib.py — the board builder underneath: outline, holes,
keep-outs, schematic linking, reference tidying, geometry checks, save
guard, DRC split, net report, render, decoupling_row() and fanout().
scripts/jlc_cpl.py — the JLCPCB placement file, with rotation
corrections kept in a sourced JSON table instead of hand edits.
- References for the conventions, the recipes' evidence, stackup and
impedance, the pcbnew API, and troubleshooting.
You place, and on request route the critical nets. Unless the user asks,
you do not route the rest.
Workflow
0. Don't clobber the user's work
--place rewrites every unlocked footprint position. Before running it:
- does the board have tracks?
check() warns;
- has the
.kicad_pcb changed since the script last wrote it? save()
refuses and backs it up to .pcbgen-backup/.
Ask the user to lock hand-placed parts in KiCad (they are then never
moved) or port them into FIXED, then use --force. Say in the handover:
re-running resets every unlocked position.
1. Make sure the board matches the schematic
No board yet? pcblib.create_board(dir, name, layers=2) writes an empty one, and
sync_footprints() fills it; stock KiCad footprint libraries resolve by nickname without
being listed in the project's fp-lib-table.
Footprints must be on the board and current. If symbols were added, removed
or had their footprint changed, sync first (delete orphans, swap changed
footprints keeping position, add new ones parked off-board, link symbols).
Board.sync_footprints() adds; swapping and deleting are in the pattern of
a project sync_pcb.py — never place onto a stale board.
2. Read the datasheets and settle the brief
Before coordinates, collect what the netlist cannot tell you. Ask the user
when you don't know:
- outline and holes (enclosure? Pi-HAT? can it grow?);
- which side(s) — single-sided is cheaper, but a module carrier is usually
double-sided;
- what must sit on an edge (connectors, antenna feeds, buttons);
- modules with no clearance underneath (a compute module on 1.5 mm mezzanine
connectors leaves none);
- card areas (M.2 on a 4.2 mm socket: only 0.90 mm parts under the card -- the card's underside
is 2.52 mm up but its own bottom parts take most of that; which side of the socket the card leaves
from, and where its standoff goes, are in
references/recipes.md);
- stackup and which nets are controlled-impedance
(
references/stackup-impedance.md).
Check that every major IC has a recipe (references/recipes.md). If one
doesn't, read its datasheet's layout section and add a recipe — match on pin
names, cite the section, and measure a reference board if one exists.
3. Write placement_config.py
A short, commented brief in the project folder. Every key is documented in
references/autoplace.md; references/example_placement_config_m2_adapter.py
is a real one (USB-C to M.2 adapter: fixed regulator block, card keep-out by
height, edge connectors).
Copy scripts/autoplace.py, recipes.py and pcblib.py into the project
so it stays self-contained.
4. Prove the model, score what exists
"C:\Program Files\KiCad\10.0\bin\python.exe" autoplace.py PROJECT --selftest
"C:\Program Files\KiCad\10.0\bin\python.exe" autoplace.py PROJECT --audit
--selftest must report a worst pad error ≤ 0.01 mm, and must have seen at
least one bottom-side part before you trust double-sided output. --audit
prints the block list (check the claims make sense — a GNSS cap claimed by
an M.2 socket means a recipe or the sheet rule needs attention) and scores
the current placement: that number is the baseline you have to beat.
5. Floor plan — the first review stop
... autoplace.py PROJECT --floorplan
Writes floorplan.svg and floorplan.json and leaves the board untouched.
Convert the SVG to PNG and look at it, then send it to the user with:
- the capacity line (free area per side vs part area — above ~50% there is
no routing room, and no solver fixes that);
- the audit result and every failing rule;
- blocks that landed somewhere the brief did not intend (wrong side, far
from their connector, RF next to a switcher);
- the decisions only the user can make (grow the board, move a module,
allow the other side).
Adjust the config or lock parts in KiCad, re-run, until the user approves.
6. Place — the second review stop
... autoplace.py PROJECT --place
Writes positions, links symbols and nets, tidies references, runs
check(), saves, runs DRC and renders. Then:
- Recipe audit: every limit, pass/fail. Report failures by name.
- DRC, two halves: placement/board-level (yours — target zero) versus
inside one footprint (library defects: fix the
.kicad_mod, then
refresh_footprints()). unconnected items equals the pin count until
routed — say so. schematic parity must be zero.
- Net spans: switch nodes a few mm, diff pairs short and direct.
- Render and look: blocks read as blocks, connectors reachable, RF clear,
references readable.
6b. Hand-written tables: decoupling rows
On a small board placed from a hand-written table (a project
place_components.py), Board.decoupling_row(ic) does what the recipes do
for autoplace: each capacitor goes just outside the IC's courtyard beside
the pin it serves, rail pad toward the pin, ground pad away, same side.
A rail's capacitors are dealt out over its pins smallest first, so every
pin gets a small one before any pin gets a second; extras stack outward.
Call it after place_all() (it overrides the caps' table entries), pass
caps=[...] to name them, and read the printed plan -- then check().
7. Critical routing (only when asked)
Placement and critical routing overlap: a regulator loop is placement and
copper. When the user asks for it:
- confirm impedance widths with the fab's calculator first
(
references/stackup-impedance.md — the listed widths are unconfirmed);
- set net classes: 90 Ω USB/PCIe, 100 Ω Ethernet, power widths by current;
- check that KiCad can see the pairs:
b.check_diff_pairs() asks
pcbnew's own DpCoupledNet() which nets it has coupled. KiCad infers pairs
from the two names — only P/N or +/-, upper case, at the end — so
USB_DP/USB_DM and TD1P/TD1M are not pairs, and on an unrecognised
pair diff_pair_width never fires (it is gated on A.inDiffPair() in
drc_engine.cpp). Both legs then route at the class's ordinary
track_width and nothing complains. Fix the names in the schematic and
re-sync; renaming on the board is undone by the next netlist import;
- route differential pairs as coupled traces, match within the pair, add a
ground via beside every signal via;
- give switching loops and battery paths pours, not thin traces;
- run DRC and report lengths. Leave the rest to the user or an autorouter
(Freerouting has no differential-pair support).
- Routing a whole board:
scripts/routelib.py (copy it beside the project's pcblib.py).
Router.pad(ref, n, expect=, net=) asserts placement and netlist, track / via / zone /
zone_connection draw, clear() makes the script idempotent, drc_report() lists every DRC item
with its board-mm position, and report() says what DRC does not: how many pieces each pour is
in, every far-layer track (each one cuts that pour), via-in-pad, pair skew.
- Hybrid routing is the default offer when the user asks for an autorouter: script the nets
where geometry is the design (switching loops, Kelvin taps, USB entry, power trunks, thermal
copper, every ground via and the pours) and give the rest to Freerouting with
Router.autoroute(scripted_nets). Its result is stored in routing/autoroute.json and drawn by
Router.replay(), so ordinary runs are byte-identical and need no Java; --autoroute re-rolls it.
Freerouting is not bundled: it needs
the jar and a Java 25 runtime in ~/tools/freerouting (or FREEROUTING_HOME) -- downloading them
is the user's call, ask first. The traps between KiCad 10 and Freerouting 2.4 are handled inside
autoroute() and listed in references/troubleshooting.md, "Hybrid routing".
Always look at a picture of an autorouted result. DRC passed a 25 mm detour under a module, a
dangling stub and a far-layer pour cut in two; report() and your eyes catch those.
- Routing changes placement. Expect to move parts once copper shows the problem, and do it in
the placement table, not by bending tracks: see "Placing for routability" in
references/placement-conventions.md.
- A whole small board can be routed from a script:
references/example_route_board.py
(2-layer, USB 2.0 pair, SOT-23-6 buck, ground pours; fabricated). The pattern that made it
safe: every pad the router starts or ends on is asserted to be where the script expects,
and every part it touches is in FIXED -- adding a test pad moved two auto-placed
capacitors and the 3.3 V track went straight through their new pads. It is idempotent
(removes and redraws tracks, vias and zones, keeping removed proxies alive), offsets a
centre line into a constant-gap pair with mitred corners, sets solid zone connection on
the regulator's ground pads, and is followed by Board.check_via_in_pad(): DRC is silent
about a same-net via inside an SMD pad, and a stitching via landed in a connector's
hold-down tab.
- USB-C receptacle pads run B7(N) A6(P) A7(N) B6(P): joining the flip pairs costs one
crossing per net. Two 1.3 mm far-layer straps as side branches, not in the signal path,
keep the ground under the pair whole. A USBLC6-2 at 180° is flow-through (D+ over the top
row, D− over the bottom); its VBUS pin then sits between the lines -- see the schematic
skill's
UsbCSink(esd_vbus=False).
- BGA escape:
Board.fanout("U1") puts a dog-bone on every inner ball:
a short track to a via in the gap diagonally outward, quadrant by
quadrant so no two balls want one site. The outer rings=1 ring is left
for top-layer escape. Via, drill, track and clearance default to the
board's own minimums (Board Setup > Constraints, floored at
0.45/0.2/0.1/0.1 mm), and it refuses when the via cannot fit between four
balls -- 0.5 mm pitch and below needs via-in-pad, a fab option. Re-running
replaces its earlier fan-out. DRC then shows one via_dangling per via
until the board is routed; that is expected, say so.
7b. JLCPCB placement file
references/example_build_package.py builds the whole upload (gerbers + drill on the same
origin as the CPL, BOM, CPL, order notes) behind a DRC gate, and refuses when the BOM and
CPL designators differ, which JLCPCB rejects. Parts with no LCSC number (holes, fiducials,
test pads) stay out of both.
python jlc_cpl.py board.kicad_pcb # -> jlc/<board>_cpl.csv
Rows are the footprints with an LCSC field, never DNP ones, positions from
the board corner (pcblib sets the aux origin there). Rotation corrections
come from references/jlc_rotations.json (skill defaults: two, each with its
source) and <project>/jlc_rotations.json (project rules and per-designator
overrides); a designator override beats a project rule beats a default.
The report lists every correction applied and every polarised or multi-pin
part with no rule -- those are the ones to look at in JLCPCB's Component
Placements preview before paying. Parts from an EasyEDA/LCSC-derived
library are usually already at JLC's zero; stock KiCad footprints are
where corrections are needed.
- Derive corrections with
scripts/jlc_compare.py board.kicad_pcb: it fetches JLCPCB's
footprint for every polarised part's LCSC number, matches it to the board's pad number by
pad number at 0/90/180/270, and prints the correction, the origin offset, the worst-pad
fit and ready-to-paste overrides. On a 47-part board: every EasyEDA-derived footprint at
zero, the four stock SOT-23 / SOT-223 at +180. It cannot see the polarity of two-pad
parts (it matches pad 1 to pad 1): confirm from their silk or symbol. The EasyEDA API
refuses requests after a burst of ~25; the tool caches and paces, so just run it again.
- Check each part's "PCBA Type" on jlcpcb.com/partdetail/Cxxxx before choosing Economic.
Radio modules are often "Standard Only" (ESP32-S3-WROOM-1U-N4 is); one such part moves the
whole order to Standard. Through-hole parts show "Wave Soldering".
- Check manufacturer names against LCSC, not memory (
wmsc.lcsc.com/ftps/wm/product/detail? productCode=Cxxxx returns brandNameEn, productModel, stockNumber): a BOM whose LCSC
numbers were all right still had 9 of 34 manufacturers wrong.
- JLCPCB's zero orientation belongs to their footprint for each LCSC part, not to the
package name. Two SOT-23-6 parts on one board needed +270 and +180. Derive it: fetch
their footprint (
uvx easyeda2kicad --lcsc_id Cxxxx --footprint), find the KiCad rotation
that puts KiCad's pad 1 where theirs is, and the correction is minus that; write it as a
per-designator override. The same comparison gives a connector's origin offset.
- Add a rule only from evidence -- a preview that showed the part turned, or
a published comparison -- and record the
source. A wrong rule rotates a
part that was right.
- Set
"verified": true once a correction was confirmed on one of the
user's own orders; unverified ones are flagged on every export.
- Bottom-side corrections are applied with the sign reversed (KiCad reports
rotation seen from the top, JLCPCB looks from below). That is not yet
confirmed on an order, so each corrected bottom row is flagged -- check
it on the first double-sided order and write down what the preview showed.
8. Deliver
- The floor plan and the top/bottom renders.
- The audit table and the DRC summary, library problems listed separately.
- What was fixed, locked, and assumed.
- The re-run command, and the warning that it resets unlocked parts.
- What you did not do.
Placement rules in brief
Details, reasons and sources: references/placement-conventions.md and
references/recipes.md.
- One function, one patch of board. The recipes define the functions.
- Switching loop first: input cap → switch → inductor → output cap, as
small as the footprints allow. Feedback on the quiet side.
- Decoupling at the pin it serves, same side, smallest value nearest.
- Follow a datasheet's layout priority list literally when it has one
(BQ25792 does).
- Connectors on edges with their mating face flush; ESD arrays beside them,
in line.
- RF (GNSS, radios) away from switchers; antenna feeds short, 50 Ω, ground
fenced.
- Nothing under a module that sits flush; only low passives under cards.
- Through-hole pads occupy both sides — never put a bottom part under one.
- Leave routing channels. Capacity above ~50% is a board-size problem.
- Like parts aligned, references readable, polarity marks visible.
pcbnew facts you will need
Full notes in references/pcbnew-api.md.
- Use KiCad's own Python:
"C:\Program Files\KiCad\10.0\bin\python.exe".
- After
board.Remove(item), never let the proxy be collected —
Board._remove() disowns and keeps it.
- A bottom footprint's geometry is mirrored then rotated; which axis depends
on the flip call. Don't reason about it —
--selftest measures it.
- Pad numbers repeat (connector shells, EP) — compare pads by order, not by
number, when checking geometry.
GetCourtyard(layer) is a real polygon; many footprints leave pads outside
it, so extent = courtyard ∪ pads.
- The netlist XML (
kicad-cli sch export netlist --format kicadxml) carries
pin names (libparts) and each symbol's sheet (sheetpath) — both drive
the recipes.
kicad-cli pcb render takes the board as the last positional argument.
- pcbnew prints
swig/python detected a memory leak at exit: harmless, on
stdout — filter it.
Gotchas that cost hours
Each is explained in references/troubleshooting.md.
- Placing onto a board whose footprints are out of date with the schematic.
- A packer "with no overlaps" still loses every loop — measure with
--audit.
- Rail capacitors are ambiguous; designator affinity and schematic sheet are
the tie-breakers, and the block list must be read, not assumed.
- A keep-out that forbids footprints flags the part it protects: forbid
copper, allow the module.
- Silkscreen below 0.15 mm gives one DRC warning per part.
- Adding one part can move every auto-placed part. Anything a later script depends on
(a router, a clearance check) belongs in
FIXED, with its pad positions asserted.
- Designators the user moves in KiCad are kept by
tidy_references() (ledger
.pcbgen-refs.json); never edit a board file while KiCad has it open (~*.lck).
- Measure, don't reason: when pcbnew behaves oddly, isolate one call.
Worked examples
references/example_placement_config_m2_adapter.py, example_route_board.py,
example_setup_fab_jlc_2layer.py, example_m2_clearance.py, example_build_models.py,
example_build_package.py — the USB-C to M.2 Key-E adapter, 30 parts, 2 layers,
generated end to end and sent to JLCPCB: hand-placed Figure 36 buck, card keep-out by
height, scripted routing, JLCPCB rules and package. They import that project's
placement_config; read them as patterns, not as a library.
1---2name: kicad-pcb-placement3description: Lay out a KiCad PCB (.kicad_pcb) from Python — netlist-driven automatic placement using datasheet-derived recipes (regulator loops, decoupling, charger priority order, hub/codec/GNSS/M.2 rules), a reviewable floor plan with lock-and-rerun, module keep-outs (compute modules / SoMs on mezzanine connectors), double-sided boards, board outline, mounting holes and tidy silkscreen, decoupling rows for hand-written tables, BGA dog-bone fan-out, scripted and hybrid routing (critical nets from a script, Freerouting for the rest, ground pours, a reproducible result), and a JLCPCB placement (CPL) file with sourced rotation corrections — and prove it with a recipe audit and KiCad's own DRC. Use this whenever the user wants parts placed or arranged on a board ("come up with a sensible PCB layout", "place the components", "floorplan this board", "where should the regulator go", "make the board smaller", "score this placement"), wants placement copied from reference designs or datasheet layout guidance, asks to route a boar4---56# KiCad PCB placement78Placement is where a board is won or lost. Routing can only connect what9placement already put within reach: a buck converter whose input cap is1030 mm from its ground pin cannot be saved by clever traces.1112The usual failure is a *pile*: parts dropped by a packer into whatever gap13fits, a regulator 50 mm from its own inductor, and a board that passes DRC14and is unroutable. A packer-style version of this skill did exactly that on a15126-part double-sided module carrier: **0 of 76** datasheet placement rules16met. The recipe-driven placer below meets **67 of 76** on the same board, and17on an unrelated 34-part single-sided board it meets 18 of 20 where a18hand-written table met 3.1920What you get:2122- **`scripts/autoplace.py`** — the placer. Reads the netlist, groups parts23 into functional blocks with recipes, solves each block, places blocks on24 the board, audits every rule, draws a floor plan, writes the board.25- **`scripts/recipes.py`** — the rules: which support parts belong to which26 IC (found by pin names, not designators), how close, in what order, with27 the datasheet or measured reference board behind each number.28- **`scripts/pcblib.py`** — the board builder underneath: outline, holes,29 keep-outs, schematic linking, reference tidying, geometry checks, save30 guard, DRC split, net report, render, `decoupling_row()` and `fanout()`.31- **`scripts/jlc_cpl.py`** — the JLCPCB placement file, with rotation32 corrections kept in a sourced JSON table instead of hand edits.33- **References** for the conventions, the recipes' evidence, stackup and34 impedance, the pcbnew API, and troubleshooting.3536You place, and on request route the critical nets. Unless the user asks,37you do not route the rest.3839## Workflow4041### 0. Don't clobber the user's work42`--place` rewrites every unlocked footprint position. Before running it:43- does the board have tracks? `check()` warns;44- has the `.kicad_pcb` changed since the script last wrote it? `save()`45 refuses and backs it up to `.pcbgen-backup/`.4647Ask the user to **lock** hand-placed parts in KiCad (they are then never48moved) or port them into `FIXED`, then use `--force`. Say in the handover:49re-running resets every unlocked position.5051### 1. Make sure the board matches the schematic52No board yet? `pcblib.create_board(dir, name, layers=2)` writes an empty one, and53`sync_footprints()` fills it; stock KiCad footprint libraries resolve by nickname without54being listed in the project's `fp-lib-table`.5556Footprints must be on the board and current. If symbols were added, removed57or had their footprint changed, sync first (delete orphans, swap changed58footprints keeping position, add new ones parked off-board, link symbols).59`Board.sync_footprints()` adds; swapping and deleting are in the pattern of60a project `sync_pcb.py` — never place onto a stale board.6162### 2. Read the datasheets and settle the brief63Before coordinates, collect what the netlist cannot tell you. Ask the user64when you don't know:6566- outline and holes (enclosure? Pi-HAT? can it grow?);67- which side(s) — single-sided is cheaper, but a module carrier is usually68 double-sided;69- what must sit on an edge (connectors, antenna feeds, buttons);70- modules with no clearance underneath (a compute module on 1.5 mm mezzanine71 connectors leaves none);72- card areas (M.2 on a 4.2 mm socket: only **0.90 mm** parts under the card -- the card's underside73 is 2.52 mm up but its own bottom parts take most of that; which side of the socket the card leaves74 from, and where its standoff goes, are in `references/recipes.md`);75- stackup and which nets are controlled-impedance76 (`references/stackup-impedance.md`).7778Check that every major IC has a recipe (`references/recipes.md`). If one79doesn't, read its datasheet's layout section and add a recipe — match on pin80names, cite the section, and measure a reference board if one exists.8182### 3. Write placement_config.py83A short, commented brief in the project folder. Every key is documented in84`references/autoplace.md`; `references/example_placement_config_m2_adapter.py`85is a real one (USB-C to M.2 adapter: fixed regulator block, card keep-out by86height, edge connectors).8788Copy `scripts/autoplace.py`, `recipes.py` and `pcblib.py` into the project89so it stays self-contained.9091### 4. Prove the model, score what exists92```bash93"C:\Program Files\KiCad\10.0\bin\python.exe" autoplace.py PROJECT --selftest94"C:\Program Files\KiCad\10.0\bin\python.exe" autoplace.py PROJECT --audit95```96`--selftest` must report a worst pad error ≤ 0.01 mm, and must have seen at97least one bottom-side part before you trust double-sided output. `--audit`98prints the block list (check the claims make sense — a GNSS cap claimed by99an M.2 socket means a recipe or the sheet rule needs attention) and scores100the current placement: that number is the baseline you have to beat.101102### 5. Floor plan — the first review stop103```bash104... autoplace.py PROJECT --floorplan105```106Writes `floorplan.svg` and `floorplan.json` and leaves the board untouched.107Convert the SVG to PNG and **look at it**, then send it to the user with:108- the capacity line (free area per side vs part area — above ~50% there is109 no routing room, and no solver fixes that);110- the audit result and every failing rule;111- blocks that landed somewhere the brief did not intend (wrong side, far112 from their connector, RF next to a switcher);113- the decisions only the user can make (grow the board, move a module,114 allow the other side).115116Adjust the config or lock parts in KiCad, re-run, until the user approves.117118### 6. Place — the second review stop119```bash120... autoplace.py PROJECT --place121```122Writes positions, links symbols and nets, tidies references, runs123`check()`, saves, runs DRC and renders. Then:124125- **Recipe audit**: every limit, pass/fail. Report failures by name.126- **DRC, two halves**: placement/board-level (yours — target zero) versus127 inside one footprint (library defects: fix the `.kicad_mod`, then128 `refresh_footprints()`). `unconnected items` equals the pin count until129 routed — say so. `schematic parity` must be zero.130- **Net spans**: switch nodes a few mm, diff pairs short and direct.131- **Render and look**: blocks read as blocks, connectors reachable, RF clear,132 references readable.133134### 6b. Hand-written tables: decoupling rows135On a small board placed from a hand-written table (a project136`place_components.py`), `Board.decoupling_row(ic)` does what the recipes do137for autoplace: each capacitor goes just outside the IC's courtyard beside138the pin it serves, rail pad toward the pin, ground pad away, same side.139A rail's capacitors are dealt out over its pins smallest first, so every140pin gets a small one before any pin gets a second; extras stack outward.141Call it after `place_all()` (it overrides the caps' table entries), pass142`caps=[...]` to name them, and read the printed plan -- then `check()`.143144### 7. Critical routing (only when asked)145Placement and critical routing overlap: a regulator loop is placement *and*146copper. When the user asks for it:147- confirm impedance widths with the fab's calculator first148 (`references/stackup-impedance.md` — the listed widths are unconfirmed);149- set net classes: 90 Ω USB/PCIe, 100 Ω Ethernet, power widths by current;150- **check that KiCad can see the pairs**: `b.check_diff_pairs()` asks151 pcbnew's own `DpCoupledNet()` which nets it has coupled. KiCad infers pairs152 from the two names — only `P`/`N` or `+`/`-`, upper case, at the end — so153 `USB_DP`/`USB_DM` and `TD1P`/`TD1M` are not pairs, and on an unrecognised154 pair `diff_pair_width` never fires (it is gated on `A.inDiffPair()` in155 `drc_engine.cpp`). Both legs then route at the class's ordinary156 `track_width` and nothing complains. Fix the names in the schematic and157 re-sync; renaming on the board is undone by the next netlist import;158- route differential pairs as coupled traces, match within the pair, add a159 ground via beside every signal via;160- give switching loops and battery paths pours, not thin traces;161- run DRC and report lengths. Leave the rest to the user or an autorouter162 (Freerouting has no differential-pair support).163- **Routing a whole board: `scripts/routelib.py`** (copy it beside the project's `pcblib.py`).164 `Router.pad(ref, n, expect=, net=)` asserts placement and netlist, `track` / `via` / `zone` /165 `zone_connection` draw, `clear()` makes the script idempotent, `drc_report()` lists every DRC item166 with its board-mm position, and `report()` says what DRC does not: how many pieces each pour is167 in, every far-layer track (each one cuts that pour), via-in-pad, pair skew.168- **Hybrid routing** is the default offer when the user asks for an autorouter: script the nets169 where geometry *is* the design (switching loops, Kelvin taps, USB entry, power trunks, thermal170 copper, every ground via and the pours) and give the rest to Freerouting with171 `Router.autoroute(scripted_nets)`. Its result is stored in `routing/autoroute.json` and drawn by172 `Router.replay()`, so ordinary runs are byte-identical and need no Java; `--autoroute` re-rolls it.173 Freerouting is not bundled: it needs174 the jar and a Java 25 runtime in `~/tools/freerouting` (or `FREEROUTING_HOME`) -- downloading them175 is the user's call, ask first. The traps between KiCad 10 and Freerouting 2.4 are handled inside176 `autoroute()` and listed in `references/troubleshooting.md`, "Hybrid routing".177 **Always look at a picture of an autorouted result.** DRC passed a 25 mm detour under a module, a178 dangling stub and a far-layer pour cut in two; `report()` and your eyes catch those.179- **Routing changes placement.** Expect to move parts once copper shows the problem, and do it in180 the placement table, not by bending tracks: see "Placing for routability" in181 `references/placement-conventions.md`.182- **A whole small board can be routed from a script**: `references/example_route_board.py`183 (2-layer, USB 2.0 pair, SOT-23-6 buck, ground pours; fabricated). The pattern that made it184 safe: every pad the router starts or ends on is asserted to be where the script expects,185 and **every part it touches is in `FIXED`** -- adding a test pad moved two auto-placed186 capacitors and the 3.3 V track went straight through their new pads. It is idempotent187 (removes and redraws tracks, vias and zones, keeping removed proxies alive), offsets a188 centre line into a constant-gap pair with mitred corners, sets solid zone connection on189 the regulator's ground pads, and is followed by `Board.check_via_in_pad()`: DRC is silent190 about a same-net via inside an SMD pad, and a stitching via landed in a connector's191 hold-down tab.192- USB-C receptacle pads run B7(N) A6(P) A7(N) B6(P): joining the flip pairs costs one193 crossing per net. Two 1.3 mm far-layer straps as side branches, not in the signal path,194 keep the ground under the pair whole. A USBLC6-2 at 180° is flow-through (D+ over the top195 row, D− over the bottom); its VBUS pin then sits between the lines -- see the schematic196 skill's `UsbCSink(esd_vbus=False)`.197- **BGA escape**: `Board.fanout("U1")` puts a dog-bone on every inner ball:198 a short track to a via in the gap diagonally outward, quadrant by199 quadrant so no two balls want one site. The outer `rings=1` ring is left200 for top-layer escape. Via, drill, track and clearance default to the201 board's own minimums (Board Setup > Constraints, floored at202 0.45/0.2/0.1/0.1 mm), and it refuses when the via cannot fit between four203 balls -- 0.5 mm pitch and below needs via-in-pad, a fab option. Re-running204 replaces its earlier fan-out. DRC then shows one `via_dangling` per via205 until the board is routed; that is expected, say so.206207### 7b. JLCPCB placement file208`references/example_build_package.py` builds the whole upload (gerbers + drill on the same209origin as the CPL, BOM, CPL, order notes) behind a DRC gate, and refuses when the BOM and210CPL designators differ, which JLCPCB rejects. Parts with no LCSC number (holes, fiducials,211test pads) stay out of both.212213```bash214python jlc_cpl.py board.kicad_pcb # -> jlc/<board>_cpl.csv215```216Rows are the footprints with an LCSC field, never DNP ones, positions from217the board corner (`pcblib` sets the aux origin there). Rotation corrections218come from `references/jlc_rotations.json` (skill defaults: two, each with its219source) and `<project>/jlc_rotations.json` (project rules and per-designator220`overrides`); a designator override beats a project rule beats a default.221The report lists every correction applied and every polarised or multi-pin222part with no rule -- those are the ones to look at in JLCPCB's Component223Placements preview before paying. Parts from an EasyEDA/LCSC-derived224library are usually already at JLC's zero; stock KiCad footprints are225where corrections are needed.226227- **Derive corrections with `scripts/jlc_compare.py board.kicad_pcb`**: it fetches JLCPCB's228 footprint for every polarised part's LCSC number, matches it to the board's pad number by229 pad number at 0/90/180/270, and prints the correction, the origin offset, the worst-pad230 fit and ready-to-paste `overrides`. On a 47-part board: every EasyEDA-derived footprint at231 zero, the four stock SOT-23 / SOT-223 at +180. It cannot see the polarity of two-pad232 parts (it matches pad 1 to pad 1): confirm from their silk or symbol. The EasyEDA API233 refuses requests after a burst of ~25; the tool caches and paces, so just run it again.234- **Check each part's "PCBA Type" on jlcpcb.com/partdetail/Cxxxx before choosing Economic.**235 Radio modules are often "Standard Only" (ESP32-S3-WROOM-1U-N4 is); one such part moves the236 whole order to Standard. Through-hole parts show "Wave Soldering".237- **Check manufacturer names against LCSC, not memory** (`wmsc.lcsc.com/ftps/wm/product/detail?238 productCode=Cxxxx` returns `brandNameEn`, `productModel`, `stockNumber`): a BOM whose LCSC239 numbers were all right still had 9 of 34 manufacturers wrong.240- **JLCPCB's zero orientation belongs to their footprint for each LCSC part, not to the241 package name.** Two SOT-23-6 parts on one board needed +270 and +180. Derive it: fetch242 their footprint (`uvx easyeda2kicad --lcsc_id Cxxxx --footprint`), find the KiCad rotation243 that puts KiCad's pad 1 where theirs is, and the correction is minus that; write it as a244 per-designator override. The same comparison gives a connector's origin offset.245- Add a rule only from evidence -- a preview that showed the part turned, or246 a published comparison -- and record the `source`. A wrong rule rotates a247 part that was right.248- Set `"verified": true` once a correction was confirmed on one of the249 user's own orders; unverified ones are flagged on every export.250- Bottom-side corrections are applied with the sign reversed (KiCad reports251 rotation seen from the top, JLCPCB looks from below). That is not yet252 confirmed on an order, so each corrected bottom row is flagged -- check253 it on the first double-sided order and write down what the preview showed.254255### 8. Deliver256- The floor plan and the top/bottom renders.257- The audit table and the DRC summary, library problems listed separately.258- What was fixed, locked, and assumed.259- The re-run command, and the warning that it resets unlocked parts.260- What you did not do.261262## Placement rules in brief263Details, reasons and sources: `references/placement-conventions.md` and264`references/recipes.md`.2652661. One function, one patch of board. The recipes define the functions.2672. Switching loop first: input cap → switch → inductor → output cap, as268 small as the footprints allow. Feedback on the quiet side.2693. Decoupling at the pin it serves, same side, smallest value nearest.2704. Follow a datasheet's layout priority list literally when it has one271 (BQ25792 does).2725. Connectors on edges with their mating face flush; ESD arrays beside them,273 in line.2746. RF (GNSS, radios) away from switchers; antenna feeds short, 50 Ω, ground275 fenced.2767. Nothing under a module that sits flush; only low passives under cards.2778. Through-hole pads occupy both sides — never put a bottom part under one.2789. Leave routing channels. Capacity above ~50% is a board-size problem.27910. Like parts aligned, references readable, polarity marks visible.280281## pcbnew facts you will need282Full notes in `references/pcbnew-api.md`.283284- Use **KiCad's own Python**: `"C:\Program Files\KiCad\10.0\bin\python.exe"`.285- **After `board.Remove(item)`, never let the proxy be collected** —286 `Board._remove()` disowns and keeps it.287- A bottom footprint's geometry is mirrored then rotated; which axis depends288 on the flip call. **Don't reason about it — `--selftest` measures it.**289- Pad numbers repeat (connector shells, EP) — compare pads by order, not by290 number, when checking geometry.291- `GetCourtyard(layer)` is a real polygon; many footprints leave pads outside292 it, so extent = courtyard ∪ pads.293- The netlist XML (`kicad-cli sch export netlist --format kicadxml`) carries294 pin names (`libparts`) and each symbol's sheet (`sheetpath`) — both drive295 the recipes.296- `kicad-cli pcb render` takes the board as the **last positional** argument.297- pcbnew prints `swig/python detected a memory leak` at exit: harmless, on298 stdout — filter it.299300## Gotchas that cost hours301Each is explained in `references/troubleshooting.md`.302303- Placing onto a board whose footprints are out of date with the schematic.304- A packer "with no overlaps" still loses every loop — measure with `--audit`.305- Rail capacitors are ambiguous; designator affinity and schematic sheet are306 the tie-breakers, and the block list must be read, not assumed.307- A keep-out that forbids footprints flags the part it protects: forbid308 copper, allow the module.309- Silkscreen below 0.15 mm gives one DRC warning per part.310- Adding one part can move every auto-placed part. Anything a later script depends on311 (a router, a clearance check) belongs in `FIXED`, with its pad positions asserted.312- Designators the user moves in KiCad are kept by `tidy_references()` (ledger313 `.pcbgen-refs.json`); never edit a board file while KiCad has it open (`~*.lck`).314- Measure, don't reason: when pcbnew behaves oddly, isolate one call.315316## Worked examples317- `references/example_placement_config_m2_adapter.py`, `example_route_board.py`,318 `example_setup_fab_jlc_2layer.py`, `example_m2_clearance.py`, `example_build_models.py`,319 `example_build_package.py` — the USB-C to M.2 Key-E adapter, 30 parts, 2 layers,320 generated end to end and sent to JLCPCB: hand-placed Figure 36 buck, card keep-out by321 height, scripted routing, JLCPCB rules and package. They import that project's322 `placement_config`; read them as patterns, not as a library.