Skip to the content
2 layers127 × 95 mmPrerouted, the printed circuit board in 3D

A fast autorouter for KiCad boards that, above all, reroutes incrementally: given the previous route, it keeps what an edit left standing and routes again only what the edit disturbed.

Size
127 × 95 mm
Copper layers
2
Thickness
1.6 mm
Parts
160
Nets
126
Pads
631
Tracks, vias
175, 277
Pours
16
Routed
Yes
KiCad
KiCad 10.0 (file 20260206)
Created
13 Oct 2026
Updated
13 Oct 2026

Overview

Prerouted is an open-source printed circuit board by bruk-io, from the bruk-io/pcb-route repository on GitHub, published under the MIT licence. It is a 2-layer board of 127 × 95 mm with 160 parts on 126 nets, routed with 175 tracks and 277 vias.

It is built around the 74AHCT1G125SE, ESP32-S3-WROOM-1-N8R2 and ESP32-CAM, with the TPS62933DRL, PCA9685PW and TPS2116DRL among its other parts. Its parts include 62 resistors, 39 capacitors, 30 connectors, 11 transistors and 10 ICs. It belongs with the ESP32 and RISC-V boards of this library.

Main components

10

README

README.md

pcb-route

A fast autorouter for KiCad boards that, above all, reroutes incrementally: given the previous route, it keeps what an edit left standing and routes again only what the edit disturbed. Nothing is locked except hand-laid copper. It reads a Specctra design and writes a Specctra session, on Freerouting's command line (-de board.dsn -do board.ses -mp 40), so it can run where Freerouting is called today.

On the one real board it has been measured on (a two-layer robot carrier, below), a full route takes 55 s against Freerouting's 337 s, and rerouting after a small edit takes 3.5 s, both clean under KiCad's DRC. It uses more vias than Freerouting (98 against 58). Freerouting can also keep existing copper, routing only what is missing; that mode has not been timed against this one yet.

Use

text
python route.py -de kicad/board.dsn -do kicad/board.ses -mp 40 --board kicad/prerouted.kicad_pcb
python route.py ... --previous golden/board.ses        # incremental; golden/prerouted.kicad_pcb is read too
FlagMeaning
-de, -do, -mpAs Freerouting: design in, session out, most negotiation rounds
--boardThe KiCad board the DSN was written from (geometry and rules come from it)
--previousA previous session of this board, to reuse
--previous-boardThe board that session was routed on (default: prerouted.kicad_pcb beside it)
--max-secondsNo new round after this; the result so far is written (default 600)
--min-widthNo track narrower than this, in mm
--seedA different net order and starting prices, for a retry
--via-costMillimetres of track one via is worth in the finished route (default 16); raise it for fewer vias, at more time. Negotiation settles with cheap vias first (2.4 mm, or --via-cost if lower); via passes then reroute the nets that have vias at this cost
--cacheWhere board exports are kept (default ~/.cache/pcb-route, or PCB_ROUTE_CACHE)

Log lines match Freerouting's: Starting auto-routing..., Auto-routing was completed in N seconds., Saving '<file>'..., plus Router: <rounds> rounds, <vias> vias, <cells> cells searched. SIGTERM finishes the net being routed and writes the result (a via pass under way is dropped). Exit code 0 when every net is routed without conflict, 2 when something is left, 1 when nothing was written.

Pads wholly inside a track keep-out (under a pour) are left for the pour, as Freerouting leaves them; the log lists them.

Install

Needs KiCad 10's Python (pcbnew; set KICAD_PYTHON if it is not /usr/bin/python3), numpy, matplotlib, and the router built with Mojo 1.1 (pip install mojo):

text
mojo build -D ASSERT=none -o router/negotiate router/negotiate.mojo

-D ASSERT=none turns off Mojo's bounds check on every collection index, which costs the router a fair share of its speed. Mojo runs on Linux and macOS (on Windows, through WSL).

Tests

python -m pytest tests (about 6 minutes). The quick tests route pcbkit's blinky example, from a pcbkit checkout beside this repository (or set PCBKIT); the rest use the robot carrier in boards/spot. tests/test_snapshots.py pins the router's output; a change that alters it fails until accepted with UPDATE_SNAPSHOTS=1 (the test's docstring says when that is right).

How it works

  1. board.py turns the KiCad board into a grid (0.05 mm cells per layer): for each net class, which net may stand in each cell, where it may drill, and each net's pads. Exports are cached; after an edit, the edited board is read as a patch of the previous export, redrawing only the grid around what changed, with the same result as a full read (tests/test_patch.py).
  2. With --previous, incremental.py works out which pieces of the previous route still stand on the edited grid; they are offered to the router as kept copper.
  3. The router (router/) routes every net with A* on the grid, then negotiates: cells two nets share get dearer every round, and the nets in conflict are routed again, until none share a cell. Kept copper is laid first and is only routed afresh if other nets keep running over it.
  4. Via passes: once the board has settled with cheap vias, vias get dearer, the nets that have them are routed again, and negotiation settles the board again. If it does not, or vias do not fall, the earlier result stands.
  5. Tidying: a grid path alternates straight and diagonal steps. Each connection is redrawn between its fixed points (ends, vias, junctions) as runs of at most two pieces, one straight and one at 45 degrees, wherever every cell of the run is clear of other copper.
  6. ses.py writes the session; reused pieces are written as they were.

Results on the robot carrier (12 Oct 2026, one 2-core cloud machine)

The board: a 127 x 95 mm two-layer carrier with about 160 parts. The edit: C2, C41, C50 and C51 moved (boards/spot/golden to boards/spot/kicad). Each session was imported into pcbnew 10 and checked with KiCad DRC at every severity, before pours.

RunWall timeCopper problemsViasTrack piecesTrack
Freerouting 1.9.0, -mp 40, full route337 s4 unconnected (3 connections at U8 left unrouted)581,4534,490 mm
pcb-route, full route, cold cache55 s (43 s routing)0981,5883,961 mm
pcb-route, full route, --via-cost 2.4 (no via passes), board export cached17 s01691,5203,612 mm
pcb-route, full route, --via-cost 40, board export cached91 s0711,7204,366 mm
pcb-route, --previous golden/..., golden export cached3.5 s0751,5594,791 mm

The incremental run keeps 96% of the previous route's track; the edited board is read as a patch in 1.6 s. Of the full route's 98 vias, about 17 are on the three U8 connections Freerouting left unrouted. No router via lands in a surface-mount pad or within the board's hole-to-hole distance of an existing hole; hand-laid tracks and vias are untouched (they are not written to the session).

Known limits

  • Measured on one real board and one small example. Dense 4- to 6-layer boards and fine-pitch BGAs are untried.
  • More vias than Freerouting. Most of the rest are crossings: a track meets another net's track on its layer, and no detour cheaper than the via cost is clear.
  • No length matching, differential pairs or impedance control.
  • --min-width above the net classes' own widths widens every track; on the carrier that left two fine-pitch pads unreachable and 3 clearance faults. At or below the class widths it is only a floor.
  • Reused copper is trusted where the edit did not change what may stand in its cells; a net whose class width or clearance changed is routed afresh.
  • The time limit and SIGTERM stop after the net being routed; a SIGTERM before routing starts (while the board is read) leaves no session.
  • The grid takes memory: about 190 MB per export of the carrier, and about 0.7 GB while routing.

Use from pcbkit

This began as the router for pcbkit, a board-as-code tool (INTENT.md has the problem it set out to solve). There:

  • build_command: the command line above, --board = the board pre wrote the DSN from.
  • pcbkit route --eco: pass --previous golden/<stem>.ses instead of copying and locking copper.
  • The retry loop: pass --seed <try number>; results are otherwise deterministic.

Files

  • route.py - the command line
  • board.py - KiCad 10 board to the router's grid (pre-routes, keep-outs, per-pad clearances)
  • incremental.py - which of the previous session's pieces still stand
  • router/ - the router: negotiate.mojo the command line, negotiation and via passes, board.mojo the grid and nets, congestion.mojo prices and footprints, search.mojo the A* search, tidy.mojo the straightening, cost.mojo step costs, buffer.mojo lazily zeroed per-cell storage, stop.mojo SIGTERM, mt19937.mojo the seeded jitter
  • ses.py - the session writer
  • check.py - import a session with pcbnew and count KiCad DRC's copper problems
  • tests/test_route.py - the command line's contract
  • tests/test_patch.py - a patched board read equals a full read, for each kind of edit
  • tests/test_snapshots.py - the router's output, pinned
  • tests/test_quality.py - tidying and via passes keep the route conflict-free, and full and incremental routes pass KiCad DRC

How the router got fast (12 Oct 2026, robot carrier)

The router began as a C++ program. Its first Mojo version was a straight port that matched it byte for byte at the same speed (a full route in 47 s against 48 s); the changes below, each measured on its own with the output compared every time, then halved that, before via passes and tidying were added:

  • Pricing a via site (the dearest cell under the via, on every layer) was over half a full route. Each row of the via's disc is scanned in SIMD vectors, and where no copper covers a vector, a cell's price is exactly 1 + history, so only the largest history is needed.
  • A via is priced only when its turn comes in the search: it is queued at the cheapest a via can be, and most queued vias never come up. The paths found are as cheap; the search takes 30% less time, and about 40% less in a via pass.
  • The A* estimate divided twice per neighbour to find its row and column; neighbours take them from the cell being expanded, and the one division left per expansion is 32-bit unsigned.
  • A cell's best cost, "seen" and "closed" marks sit in one 16-byte record, so a visit reads one cache line, not three.
  • Counting a net's keep-out footprint was 40% of an incremental run. Rows are tested 16 cells at a time and only unmarked cells are visited; when a path cell is next to the one before it, only the edge of the disc that the step uncovers can hold unmarked cells.
  • Page faults were a third of an incremental run's time. Grid-sized buffers are mapped with huge pages (Linux), and the board's grids are read straight into them: 200k faults down to 15k.

Where the time goes as --via-cost rises: the search, not the arithmetic. Once a track is blocked on its layer, A* has to try every detour cheaper than a via before it takes one, so the cells searched grow with about the square of the via cost (79 M cells at 2.4 mm, 221 M at 16 mm, 520 M at 40 mm), at about 190 ns each. On the default full route: negotiation 17 s, the first via pass 20 s, the second 4 s, tidying under 1 s. Charging the higher via cost through every negotiation round instead of in via passes costs 2-5 times the time for the same vias.

Tried and dropped: packing occupancy, kept copper and history into one record per class (no faster); skipping a via's price when every other layer is already reached more cheaply (under 0.1% of sites); counting the via a cell's layer forces into the A* estimate (target pads are rarely on one layer only); restarting the sharing price for a via pass (never settled); taking vias out while tidying through clear cells only (8 vias for 4 s; a via pass does better).

Notes on Mojo 1.1: List.read_bytes() took 0.45 s on the 200 MB board file against 0.17 s for C's fread, so grids are read into buffers directly. The Mojo runtime runs threads of its own, so blocking SIGTERM on the main thread is not enough: a C-ABI handler passes a stop signal on to the main thread, which polls for it between nets. An environment variable set to an empty value counts as unset, and numbers must parse cleanly.

Parts

160
QtyValueFootprintReferences
16220Resistor_SMD:R_0603_1608MetricR60, R61, R62, R63, R64, R65, R66, R67, R68, R69, R70, R71, R72, R73, R74, R75
15100nCapacitor_SMD:C_0603_1608MetricC2, C3, C4, C6, C12, C13, C19, C20, C31, C41, C46, C47, C48, C50, C80
1310kResistor_SMD:R_0603_1608MetricR2, R6, R13, R15, R19, R30, R31, R42, R44, R50, R82, R84, R91
8BSC010N04LSPackage_TO_SOT_SMD:TDSON-8-1Q1, Q2, Q3, Q4, Q8, Q9, Q10, Q11
7100kResistor_SMD:R_0603_1608MetricR10, R16, R17, R40, R88, R89, R90
5470u 16VCapacitor_SMD:CP_Elec_8x10C5, C42, C43, C44, C45
410uCapacitor_SMD:C_0805_2012MetricC18, C24, C51, C81
422uCapacitor_SMD:C_1206_3216MetricC15, C16, C22, C23
3100Resistor_SMD:R_0603_1608MetricR7, R8, R45
32N7002Package_TO_SOT_SMD:SOT-23Q5, Q6, Q7
210Resistor_SMD:R_0603_1608MetricR4, R5
210uCapacitor_SMD:C_1206_3216MetricC10, C11
215kResistor_SMD:R_0603_1608MetricR83, R85
21kResistor_SMD:R_0603_1608MetricR20, R87
22.2kResistor_SMD:R_0603_1608MetricR51, R52
222Resistor_SMD:R_0603_1608MetricR23, R24
2220pCapacitor_SMD:C_0603_1608MetricC25, C26
222uCapacitor_SMD:C_0805_2012MetricC17, C30
233nCapacitor_SMD:C_0603_1608MetricC14, C21
24.7kResistor_SMD:R_0603_1608MetricR80, R81
2470nCapacitor_SMD:C_0603_1608MetricC1, C40
247kResistor_SMD:R_0603_1608MetricR11, R43
25.1kResistor_SMD:R_0603_1608MetricR21, R22
2LM74502-Q1Package_TO_SOT_SMD:SOT-23-8U1, U3
2QWIICConnector_JST:JST_SH_BM04B-SRSS-TB_1x04-1MP_P1.00mm_VerticalJ30, J31
2TPS62933Package_TO_SOT_SMD:SOT-583-8U5, U7
11mResistor_SMD:R_Shunt_Isabellenhuette_BVR4026R3
11uCapacitor_SMD:C_0603_1608MetricC32
131.6kResistor_SMD:R_0603_1608MetricR18
1330Resistor_SMD:R_0603_1608MetricR86
135.7kResistor_SMD:R_0603_1608MetricR14
138.3kResistor_SMD:R_0603_1608MetricR1
14.7uInductor_SMD:L_Bourns_SRP6060FAL2
149.9kResistor_SMD:R_0603_1608MetricR41
152.3kResistor_SMD:R_0603_1608MetricR12
16.8uInductor_SMD:L_Bourns_SRP6060FAL1
174AHCT1G125Package_TO_SOT_SMD:SOT-23-5U9
1ALWAYS ONJumper:SolderJumper-2_P1.3mm_Open_RoundedPad1.0x1.5mmJP1
1BATTERY 2SConnector_AMASS:AMASS_XT60PW-M_1x02_P7.20mm_HorizontalJ1
1BOOTButton_Switch_SMD:SW_SPST_TL3342SW2
1CH0 FLSspotmicro:PinHeader_1x03_P2.54mm_Vertical_ServoJ10
1CH1 FLUspotmicro:PinHeader_1x03_P2.54mm_Vertical_ServoJ11
1CH10 RRUspotmicro:PinHeader_1x03_P2.54mm_Vertical_ServoJ20
1CH11 RRLspotmicro:PinHeader_1x03_P2.54mm_Vertical_ServoJ21
1CH12 AUX12spotmicro:PinHeader_1x03_P2.54mm_Vertical_ServoJ22
1CH13 AUX13spotmicro:PinHeader_1x03_P2.54mm_Vertical_ServoJ23
1CH14 AUX14spotmicro:PinHeader_1x03_P2.54mm_Vertical_ServoJ24
1CH15 AUX15spotmicro:PinHeader_1x03_P2.54mm_Vertical_ServoJ25
1CH2 FLLspotmicro:PinHeader_1x03_P2.54mm_Vertical_ServoJ12
1CH3 FRSspotmicro:PinHeader_1x03_P2.54mm_Vertical_ServoJ13
1CH4 FRUspotmicro:PinHeader_1x03_P2.54mm_Vertical_ServoJ14
1CH5 FRLspotmicro:PinHeader_1x03_P2.54mm_Vertical_ServoJ15
1CH6 RLSspotmicro:PinHeader_1x03_P2.54mm_Vertical_ServoJ16
1CH7 RLUspotmicro:PinHeader_1x03_P2.54mm_Vertical_ServoJ17
1CH8 RLLspotmicro:PinHeader_1x03_P2.54mm_Vertical_ServoJ18
1CH9 RRSspotmicro:PinHeader_1x03_P2.54mm_Vertical_ServoJ19
1ESP32-CAMConnector_JST:JST_XH_B7B-XH-A_1x07_P2.50mm_VerticalJ37
1ESP32-S3-WROOM-1-N8R2RF_Module:ESP32-S3-WROOM-1U8
1EXPANSIONConnector_PinHeader_2.54mm:PinHeader_1x10_P2.54mm_VerticalJ39
1FROM SERVO BUCKConnector_AMASS:AMASS_XT60PW-F_1x02_P7.20mm_HorizontalJ3
1GreenLED_SMD:LED_0603_1608MetricD2
1IMUConnector_JST:JST_XH_B5B-XH-A_1x05_P2.50mm_VerticalJ33
1INA226Package_SO:MSOP-10_3x3mm_P0.5mmU2
1LED RINGConnector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ36
1OLEDConnector_JST:JST_XH_B4B-XH-A_1x04_P2.50mm_VerticalJ32
1PCA9685PWPackage_SO:TSSOP-28_4.4x9.7mm_P0.65mmU4
1POWER BTNConnector_JST:JST_XH_B4B-XH-A_1x04_P2.50mm_VerticalJ38
1RESETButton_Switch_SMD:SW_SPST_TL3342SW1
1SMBJ8.5ADiode_SMD:D_SMBD1
1SMCJ7.0ADiode_SMD:D_SMCD3
1SONAR LConnector_JST:JST_XH_B4B-XH-A_1x04_P2.50mm_VerticalJ34
1SONAR RConnector_JST:JST_XH_B4B-XH-A_1x04_P2.50mm_VerticalJ35
1TO SERVO BUCKConnector_AMASS:AMASS_XT60PW-F_1x02_P7.20mm_HorizontalJ2
1TPS2116Package_TO_SOT_SMD:SOT-583-8U6
1USB-CConnector_USB:USB_C_Receptacle_HRO_TYPE-C-31-M-12J4
1USBLC6-2SC6Package_TO_SOT_SMD:SOT-23-6U10

Similar boards

  • 8 layers140 × 314.2 mmRabbit body c, the printed circuit board in 3D

    Rabbit body c

    seralexeev/rabbit

    rabbit source code

    673 parts1
  • 2 layers79 × 59.5 mmCATSTNC R2, the printed circuit board in 3D

    CATSTNC R2

    CamK06/CATS-Base-Hardware

    An RP2040-based CATS transceiver

    77 parts3
  • 4 layers112 × 75 mmKvm board, the printed circuit board in 3D

    Kvm board

    FuNK3Y/picoKVM

    A network KVM that can switch inputs of smart monitors

    95 parts6
  • 4 layers126 × 90 mmIncubator controller, the printed circuit board in 3D

    Incubator controller

    tanaes/OvoEasy_incubator_controller

    KiCad design for the OvoEasy incubator controller board

    169 parts
  • 4 layers144 × 127 mmFrothfet, the printed circuit board in 3D

    Frothfet

    hoeken/frothfet

    Yarrboard load controller board with current sense, volt sense, fuses, and sender support.

    394 parts14GPL-3.0
  • 4 layers190 × 110 mmGeophys mainboard, the printed circuit board in 3D

    Geophys mainboard

    Benjamin-girard-1/magnetometer-node-old

    384 parts
  • 6 layers131.5 × 132.4 mmBedCooler, the printed circuit board in 3D

    BedCooler

    NicholasKorth/BedCooler

    A dual-zone heating and cooling controller for a bed: a 6-layer KiCad board with an STM32G474 real-time controller and a Raspberry Pi Compute Module 5 host.

    258 parts11
  • 4 layers100 × 80 mmRBPS, the printed circuit board in 3D

    RBPS

    riteshsalikeripatil/Battery_Management_System

    PCB and schematic design files for the RBPS hardware project.

    98 parts1

Questions

How big is the Prerouted PCB?

It measures 127 × 95 mm and has 2 copper layers, 1.6 mm thick.

What parts does the Prerouted use?

160 parts, built around the 74AHCT1G125SE, ESP32-S3-WROOM-1-N8R2, ESP32-CAM and TPS62933DRL. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.

Where can I download the Prerouted KiCad files?

From the bruk-io/pcb-route repository on GitHub, at the commit this page was made from: the board is boards/spot/golden/prerouted.kicad_pcb. PCBKit opens the board in the browser, with nothing to install.

Can I use the Prerouted design in my own project?

Its repository publishes it under the MIT licence: you may reuse it on that licence's terms (keep the author's notice, and for a copyleft licence share your changes alike).