Compare commits

6 Commits

Author SHA1 Message Date
onur 1218c33aa4 Add the fabrication spec and document the RF constraints a reviewer flagged
Four RF points came back from review. Two were real gaps, two were already
sound but undocumented, and one premise was stale.

Real gap 1 - nothing told the fab this is a controlled-impedance board.
docs/FABRICATION.md now carries the stackup (JLC08161H-3313, layer by layer
with dielectric heights and Er), the two impedance targets (RF50_CPWG 50 ohm at
0.164/0.200 on F.Cu over L2, USB90_DIFF 90 ohm at 0.135/0.150), the process
minima, and an explicit request that the vendor confirm the widths with their
own field solver. It records that JLC models this as
CoatedCoplanarWaveguideWithLowerGnd1B assuming 1.6 mil finished outer copper,
0.6 mil soldermask at Er 3.8 and top width = base - 0.7 mil, and that copper
thickness alone moves this geometry by more than 10 ohm - which is why the
number has to be fixed by the vendor's model rather than ours. It also states
the decision rule: 44 ohm is still 24 dB return loss and not the worst term in
the chain, so the geometry should not be changed unless the vendor reports
below that.

Real gap 2 - the active-antenna requirement was implied everywhere but never
quantified. hardware/README.md and docs/INTERFACES.md now state it plainly:
with both internal LNAs off the mixer input is the first active stage at
10.3 dB NF, so a ~25 dB active antenna gives ~1.65 dB system NF while a passive
antenna puts it above 12 dB and the receiver is unusable. The same section now
explains why the SAW is not optional either.

Documented, not changed - the launch relief. Its ~37 ohm / 30 dB figure is a 2D
quasi-static estimate of a genuinely 3D transition (connector, board edge, pad
overhang) and is now labelled as such, with 3D EM or VNA named as what would
settle it. The unrelieved case was 10.7 ohm / 12 dB, so the improvement holds
regardless of the exact figure.

Documented, not changed - the bias choke. Its 2.1 GHz SRF sits above 1575 MHz,
so the choke is still inductive and the proximity raises |Z| from 554 to about
1270 ohm, which helps a bias tee rather than hurting it. Swept +/-10% on both L
and SRF: |Z| stays above 900 ohm, return loss above 30 dB, insertion loss below
0.003 dB.

Stale premise - the review tied the impedance to "the inconsistent stackup".
That inconsistency was fixed in 791ca5c, and the F.Cu-to-L2 construction that
sets the RF impedance (3313 prepreg, 0.0994 mm, Er 4.1) was in fact identical
in both the old ten-layer and the current eight-layer stacks, so the CPWG
geometry never actually moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012KaTmCCxCEW4awsmXRNT1d
2026-09-01 18:19:22 +03:00
onur 791ca5c9fa Restack the generator and source board to eight layers, matching the deliverable
The routed deliverable has always been eight layers (JLC08161H-3313), but
generate_pcb.py still emitted ten and hardware/board.kicad_pcb was itself still
a ten-layer board. Source and generator agreed with each other and disagreed
with what is actually being fabricated, so running the documented regeneration
flow would have silently replaced the eight-layer design with a conflicting
ten-layer one and destroyed the routing.

generate_pcb.py now builds the real stack: SetCopperLayerCount(8), no L8_PWR /
L9_SIG, and the STACKUP string taken verbatim from board_routed.kicad_pcb - the
two are now byte-identical. Silkscreen reads JLC08161H-3313 8L instead of
JLC10161H 10L, and the title block follows.

The zone layout follows the same correction. With only one dedicated power
plane there is no third plane for the two big FPGA I/O rails, so they are
carried as islands on the routing layers that have room: VCC1V8 on L6 and
VCC3V3 on B.Cu, replacing the ten-layer arrangement of a VCC3V3 background on
L8 and a board-wide GND pour on B.Cu. The regenerated board now reproduces the
routed board's 26 copper zones exactly (the 27th, the F.Cu RF ground pour, is
added later by finalize_routing).

Also drops the In7_Cu/In8_Cu remnants from finalize_routing's RF_NO_DIGITAL
layer set and route_local_power's RF_REGION_LAYERS - those layers do not exist
on this board, so they were dead bits in a layer mask - and corrects the
JLC10161H reference in board.kicad_dru.

The placement check now reports 21 isolated_copper warnings rather than 17.
That is expected and explained in README.md: on an unrouted board the I/O-rail
islands on L6 and B.Cu have nothing joining them yet. The routed board still
reports zero.

Verified: regenerated board is 8 layers with a stackup identical to the routed
board, 312 footprints (311 + R108), TEMP keepouts at their corrected extents.
design_data assertions pass, ERC 0/0, routed board still 0 DRC violations,
0 unconnected, every net one copper group, parity unchanged at 402.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012KaTmCCxCEW4awsmXRNT1d
2026-09-01 13:26:37 +03:00
onur 4fa38e35bd Fix the RF launch: relieve the planes under J2 and put the ESD on the line
The 0.164/0.200 CPWG geometry was fine; the calculation just stopped at the
trace and never covered the connector launch, which is where the real
discontinuity was.

J2's centre land is 1.5 mm wide because that is what the connector's centre pin
needs mechanically. Sitting on solid L2_GND only 0.0994 mm below, that land is
a 10.7 ohm transmission line, not a 50 ohm one. The 1.84 mm of it over the
plane is 6.7 degrees at L1 and reflects 12 dB (VSWR 1.67) - by far the worst
discontinuity in the chain, and worse than everything the carefully built
0.164 mm line downstream avoids.

RF_LAUNCH_RELIEF voids L2_GND and L3_SIG under the land, (172.35, 40.95) to
(175.10, 43.05). The reference drops to L4_PWR - VCC3V0_RF, an AC ground -
0.587 mm below, which raises the launch to 37 ohm and 30 dB. L3 carried no
pour and no copper there, so only L2 actually had to go; the area covers L3 as
well so a later edit cannot fill the gap back in. It stops where the land ends
and the 0.164 mm line begins, verified: L2 GND is absent across the whole land
and present again at x = 172.0 and inboard.

D1's ESD clamp sat at the end of a 1.34 mm spur, where its 0.3 pF presented
-j228 ohm and cost 3.4 dB for nothing. D1 is rotated onto the run at
(171.30, 42.416) so pad 1 overlaps the line directly and pad 2 lands in the
RF ground pour; the spur is deleted.

Return loss, worst term first:
  SMA launch land      12.0 -> 30.0 dB   (10.7 -> 37.0 ohm)
  D1 ESD clamp         19.2 -> 22.6 dB   (now just the part's own 0.3 pF)
  C85 DNP tuning stub  25.5 dB unchanged, intentional provision
  C84 DNP land in line 30.1 dB unchanged, negligible
  worst-case coherent   7.0 -> 14.4 dB
The dominant term is now a component choice rather than a layout defect.

Verified: ERC 0/0, DRC 0 violations, 0 unconnected, every net one copper group,
parity unchanged at the 402 pre-existing warnings, RF still 10 segments all on
F.Cu at exactly 0.164 mm.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012KaTmCCxCEW4awsmXRNT1d
2026-09-01 12:55:49 +03:00
onur 1ea2bc64ac Add the missing TPS2121 status pull-up (R108) and correct the power-mux docs
The power mux's status output had no pull-up, which broke the USB current
budget interlock.

TPS2121 ST is an open-drain output; the datasheet lists RST = 6-20 kOhm under
Recommended Operating Conditions, alongside RILM, so it is an external part.
ST is driven low only while IN2 (the J3 external 5 V header) is sourcing OUT,
and is high impedance whenever IN1 (the J7 USB-C input) is sourcing. U10 gate 2
inverts MUX_ST into EXT_SELECTED, and MAIN_EN = USB_3A_OK OR EXT_SELECTED
enables the 15 A core buck. With MUX_ST floating on USB-C power, a drift low
asserts MAIN_EN no matter what the TUSB320 advertised, so the board would try
to bring up the core rail on a 500 mA port. Every other open-drain net here
already has its pull-up (R7/R8, R30, R33/R34, R98/R104/R105/R106); this one was
missed.

R108 (10 k, VCC3V3_AUX -> MUX_ST) is appended at the end of design_data.py
rather than beside the mux: reference designators are assigned in call order,
so inserting it mid-file would renumber every later resistor and invalidate the
routed board. Placed at (53.0, 101.6), 2.4 mm from the U10 gate it holds up,
on a site checked clear of existing copper, and routed with close_remaining.py.

Also corrects POWER_TREE.md, which credited PR1 for giving the external input
priority. PR1 = GND actually *disables* priority mode; CP2 (tied to VCC3V3_AUX)
is what selects IN2. Since CP2 derives from the mux's own output the behaviour
is two-phase - highest-voltage wins at cold start, IN2 unconditionally once the
aux rail is up - so with both inputs connected there can be one seamless
switchover at startup. Documented, with the numbers: both eFuses UV 3.98 V /
OV 5.80 V, ILIM 2.69 A (USB-C) and 4.04 A (header), mux OV 5.69 V / ILIM 4.5 A.
The two inputs are never summed: both eFuses block reverse current and the mux
bonds exactly one input at a time.

board_routed.kicad_sch/.kicad_sym are now gitignored byte copies of the tracked
schematic; kicad-cli needs them beside the board for --schematic-parity, and
deleting them in the previous commit had silently disabled that check.

Verified: ERC 0/0, DRC 0 violations, 0 unconnected, every net one copper group,
schematic parity unchanged at the 402 pre-existing warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012KaTmCCxCEW4awsmXRNT1d
2026-09-01 12:28:00 +03:00
onur 95609a6b6a Complete the routing: 0 DRC violations, 0 unconnected
The board now routes end to end. Every net carrying two or more pads is a
single connected copper group (216 nets, 1175 pads); kicad-cli reports 0 DRC
violations and 0 unconnected items with zones refilled.

Freerouting was never the blocker. The 14 remaining opens were three separate
defects:

1. Obstacles in route_local_power.py modelled every pad as its axis-aligned
   bounding box. A 0.8-mm BGA dogbone site sits diagonally between four lands:
   true clearance 0.1407 mm, but the box test measured 0.4 mm from the box edge
   and rejected it, so no via could be placed anywhere in the Ti375 escape
   field. Replaced with an exact rounded-rectangle model, validated against
   KiCad's own SHAPE::Collide to 0.00 um over 14850 samples. Same 0 DRC and one
   fewer unconnected at the route_local_power stage.

2. CLOCK's 0.15-mm clearance cannot physically escape the BGA: two adjacent
   0.45-mm dogbone vias on 0.8-mm pitch leave a 0.35-mm channel and a 0.10-mm
   trace needs 0.40 mm. HARD_TCK and MAX_SCLK could never leave the package.
   New rule "Ti375 escape field copper clearance" relaxes clearance to the
   0.10 mm this field already uses, only where both objects are inside U1's
   courtyard. The clock trunk keeps 0.15 mm outside it.

3. RF_NO_DIGITAL was a rectangle reaching 7 mm past the RF line, covering the
   MAX2771's whole digital pin field and forcing every U21 output onto F.Cu
   alone. Narrowed to the corridor generate_pcb.py already reserves as
   TEMP_RF_LINE (y <= 43.7), clearing RF copper by 1.1 mm. The RF return path
   is guaranteed by the unrouted L2/L7 ground planes, not by this area.

scripts/close_remaining.py replaces the ad-hoc residual scripts: a multi-layer
maze over (x, y, layer) at 0.1 mm on all five routable layers, seeded from all
copper of both groups, exact clearances, 0.10-mm necking inside the escape
field, and reversible rip-up that rolls back entirely if a displaced net cannot
be put back. ~150 s for the whole board.

Also removes the regenerable intermediate boards, one-off session logs and the
superseded residual scripts, and tracks board_routed.* as the deliverable.

The 402 schematic-parity warnings are pre-existing and unchanged: 199
auto-named unconnected pins, 199 footprint library-nickname mismatches, and 4
mounting holes with no symbol. None affects connectivity or fabrication.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012KaTmCCxCEW4awsmXRNT1d
2026-09-01 11:58:41 +03:00
onur af7c43b2a8 Restack to four signal layers and route the board
The Ti375 fanout could not be resolved with three routing layers, so L5
stops being a dedicated 1.8-V plane and becomes a signal layer.  No rail
owns a whole layer any more: the supplies are split into zone islands on
L4 and L6, L2 and L7 stay solid and completely unrouted, and F.Cu, L3, L5
and B.Cu are all available for signals.

Routing is staged rather than handed to the autorouter wholesale.  The
BGA escape, the buck high-current loops, plane stitching and the local
power completion all run first and are locked; Freerouting then sees only
the ordinary digital classes, with everything else present as fixed
copper.  finalize_routing.py installs the audited RF and USB geometry and
closes whatever the autorouter left.

Four bugs behind the routing failures, all of them the same shape - a
layer list written down instead of read from the board, which was correct
while L5 was a plane and silently wrong afterwards:

- export_routing_stage.py emitted via keepouts for three layers only, so
  every existing via was invisible to the router on L5.  It drove traces
  straight through them: 102 shorting_items in DRC.
- route_local_power.py never used L5 at all, and picked up escape vias on
  two layers instead of all four.
- add_rf_ground() judged fence positions against vias and pads but not
  tracks.  It runs after the session import, when that region already has
  routed copper, and dropped a ground via on top of a signal trace.

route_local_power.py also grew a via cloud on every rail: reaching a
plane counted as progress even when it merged nothing, so the same pad
collected a via per pass, 0.4 mm further out each time.  Progress is now
measured by the group count actually falling, and groups already sitting
on their plane are skipped.  That removed 404 redundant vias.

The MAX2771 PLL loop filter was spread across three corners of the
package, which forced the charge-pump node onto an inner layer under the
GPS front end.  All three parts now sit in the pocket at U21's north-west
corner and the net stays on F.Cu.  A permanent RF_NO_DIGITAL area plus a
scoped rule keeps that region free of non-power copper on L3/L5/L6/B.Cu,
and the local router enforces the same restriction, since it is the one
stage that runs after the temporary keepout is deleted.

Also enables the solder-mask bridge check that was switched off
board-wide; the board passes it with no violations.

Final board: DRC clean - no shorts, clearance, disallowed-item or
dangling-copper violations - with 26 unconnected endpoints left.  Routing
is spread 37% L3, 27% F.Cu, 26% L5, 10% B.Cu over 6548 mm and 971 vias.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 01:41:43 +03:00
42 changed files with 209977 additions and 60275 deletions
+15 -2
View File
@@ -14,11 +14,22 @@ __pycache__/
# Local manufacturer documents; links and policy are in references/README.md # Local manufacturer documents; links and policy are in references/README.md
references/official/ references/official/
# Stale/intermediate routed board variants # Stale/intermediate routed board variants, regenerated by the routing flow.
# board_routed.kicad_pcb is the finished deliverable and IS tracked.
hardware/board_prerouted.* hardware/board_prerouted.*
hardware/board_plane_stitched.* hardware/board_plane_stitched.*
hardware/board_power_routed.* hardware/board_power_routed.*
hardware/board_routed.* hardware/board_power_local.*
# kicad-cli finds the schematic for --schematic-parity by matching the board's
# own basename, so board_routed needs a copy of board.kicad_sch beside it.
# It is a byte copy of the tracked original, so it is not tracked itself:
# cp hardware/board.kicad_sch hardware/board_routed.kicad_sch
# cp hardware/board.kicad_sym hardware/board_routed.kicad_sym
hardware/board_routed.kicad_sch
hardware/board_routed.kicad_sym
# KiCad writes these when it rescues a symbol; the originals are tracked.
hardware/rescue-backup/
# Routing/build workspace. Keep only the current review deliverables. # Routing/build workspace. Keep only the current review deliverables.
outputs/* outputs/*
@@ -27,4 +38,6 @@ outputs/*
!outputs/jlcpcb_bom.csv !outputs/jlcpcb_bom.csv
!outputs/erc_final_reset.rpt !outputs/erc_final_reset.rpt
!outputs/drc_final_placement.rpt !outputs/drc_final_placement.rpt
!outputs/drc_final_routed.rpt
!outputs/board_final_reset_top.png !outputs/board_final_reset_top.png
!outputs/routing_report.txt
+20 -2
View File
@@ -83,6 +83,19 @@ python3 scripts/generate_design_tables.py
/usr/bin/python3 scripts/generate_pcb.py /usr/bin/python3 scripts/generate_pcb.py
``` ```
Routing, aynı kaynaklardan tekrar üretilebilen aşamalı bir akıştır. Sırayla
BGA escape, plane bağlantıları, buck güç katı, yerel güç tamamlama, sinyal
autorouting ve denetlenmiş RF/USB geometrisi uygulanır:
```bash
/usr/bin/python3 scripts/preroute_bga.py
/usr/bin/python3 scripts/stitch_plane_pads.py
/usr/bin/python3 scripts/route_power_stage.py
/usr/bin/python3 scripts/route_local_power.py
```
Devamı ve autorouter komutları için [routing akışı](scripts/README.md).
Kontrol: Kontrol:
```bash ```bash
@@ -92,9 +105,13 @@ kicad-cli pcb drc --severity-all \
-o outputs/drc_final_placement.rpt hardware/board.kicad_pcb -o outputs/drc_final_placement.rpt hardware/board.kicad_pcb
``` ```
Güncel placement kontrolünde ERC 0 ihlaldir. DRC'deki 12 `isolated_copper` Güncel placement kontrolünde ERC 0 ihlaldir. DRC'deki 21 `isolated_copper`
uyarısı ve 499 açık bağlantı, kartın henüz route edilmemiş olmasından uyarısı ve 499 açık bağlantı, kartın henüz route edilmemiş olmasından
kaynaklanır; üretim öncesi kabul edilebilir final sonuç değildir. kaynaklanır; üretim öncesi kabul edilebilir final sonuç değildir. Sayının
yüksek olmasının nedeni, sekiz katmanda iki büyük I/O rayının kendi plane
katmanı olmaması ve L6/B.Cu üzerinde ada olarak taşınmasıdır; route edilmemiş
kartta bu adaları henüz hiçbir şey birleştirmez. Route edilmiş kartta
`isolated_copper` sayısı sıfırdır.
## Kaynakların önceliği ## Kaynakların önceliği
@@ -114,6 +131,7 @@ ve placement kararları önce ilgili Python kaynağına işlenmelidir.
- [MAX2771, SCH16T, QSPI, FTDI ve test noktası bağlantıları](docs/INTERFACES.md) - [MAX2771, SCH16T, QSPI, FTDI ve test noktası bağlantıları](docs/INTERFACES.md)
- [Ti375 ball, bank, gerilim ve PMOD pin seçimi](docs/FPGA_PINOUT.md) - [Ti375 ball, bank, gerilim ve PMOD pin seçimi](docs/FPGA_PINOUT.md)
- [Ayrıntılı güç ağacı, sıralama ve reset davranışı](docs/POWER_TREE.md) - [Ayrıntılı güç ağacı, sıralama ve reset davranışı](docs/POWER_TREE.md)
- [Üretim ve kontrollü empedans şartnamesi](docs/FABRICATION.md)
- [Üretim, kontrol ve routing akışı](scripts/README.md) - [Üretim, kontrol ve routing akışı](scripts/README.md)
- [Pin tabloları ve türetilmiş veriler](docs/README.md) - [Pin tabloları ve türetilmiş veriler](docs/README.md)
- [Güncel çıktılar ve raporların anlamı](outputs/README.md) - [Güncel çıktılar ve raporların anlamı](outputs/README.md)
+80
View File
@@ -0,0 +1,80 @@
# Üretim ve kontrollü empedans şartnamesi
Bu dosya üreticiye verilecek şartnamedir. Gerber ve drill dosyaları tek
başlarına kontrollü empedansı ısmarlamaz; sipariş sırasında aşağıdaki stackup
ve empedans hedefleri açıkça belirtilmelidir.
## Kart
| Öğe | Değer |
|---|---|
| Boyut | 155.05 x 90.05 mm |
| Katman | 8 |
| Kalınlık | 1.6 mm |
| Stackup | **JLC08161H-3313** (JLCPCB standart 8 katman) |
| Dış bakır | 1 oz baz |
| İç bakır | 0.5 oz |
| Yüzey | ENIG |
| Min hat/aralık | 0.10 mm / 0.10 mm |
| Min delik | 0.225 mm |
| Min annular ring | 0.1125 mm |
## Katman dizilimi
```
F.Cu sinyal + RF 35 um
prepreg 3313 0.0994 mm er 4.1 <-- RF empedansını bu belirler
L2_GND kesintisiz GND 15.2 um
core 0.1000 mm er 4.36
L3_SIG sinyal 15.2 um
prepreg 3x2116 0.3568 mm er 4.16
L4_PWR güç plane 15.2 um
core 0.3000 mm er 4.41
L5_SIG sinyal 15.2 um
prepreg 3x2116 0.3568 mm er 4.16
L6_SIG sinyal + VCC1V8 15.2 um
core 0.1000 mm er 4.36
L7_GND kesintisiz GND 15.2 um
prepreg 3313 0.0994 mm er 4.1
B.Cu sinyal + VCC3V3 35 um
```
## Kontrollü empedans hedefleri
| Net sınıfı | Yapı | Katman | Referans | Geometri | Hedef |
|---|---|---|---|---|---|
| `RF50_CPWG` | Coplanar waveguide + alt GND | F.Cu | L2_GND | hat 0.164 mm, GND aralığı 0.200 mm | 50 ohm tek uçlu |
| `USB90_DIFF` | Coplanar diferansiyel çift | F.Cu | L2_GND | hat 0.135 mm, çift aralığı 0.150 mm | 90 ohm diferansiyel |
**Üreticiden istenen:** yukarıdaki hedefler için kendi field-solver'ı ile hat
genişliğini doğrulaması ve gerekirse düzeltilmiş genişliği bildirmesi.
JLCPCB'nin modeli `CoatedCoplanarWaveguideWithLowerGnd1B`'dir ve 1 oz dış bakır
için 1.6 mil bitmiş bakır kalınlığı, 0.6 mil solder mask (er 3.8) ve
taban genişliği - 0.7 mil üst genişlik varsayar.
Tasarımın kendi hesabı 0.164 mm / 0.200 mm için yaklaşık 44-50 ohm verir
(bkz. `scripts/finalize_routing.py` `route_rf`). Bakır kalınlığı bu geometride
sonucu 10 ohm'dan fazla kaydırdığı için nihai değer üreticinin modeliyle
sabitlenmelidir. 44 ohm bile 24 dB return loss demektir ve RF zincirinin en
kötü terimi değildir, dolayısıyla üretici 44 ohm'un altını bildirmedikçe
geometri değiştirilmemelidir.
## Alan çözücü ile doğrulanması gereken tek yapı
J2 edge-launch SMA pad'i 1.5 mm genişliğindedir ve 0.164 mm'lik hattan çok
daha geniştir. Pad'in altında L2 ve L3 `RF_LAUNCH_RELIEF` ile boşaltılmıştır;
referans 0.587 mm aşağıdaki L4'e iner. Tasarımın 2B yarı-statik tahmini bu
launch için yaklaşık 37 ohm ve 30 dB return loss verir.
Bu bir 2B tahmindir. Konnektör geçişi, kart kenarı ve pad taşması gerçek bir
3B yapıdır; kesin değer 3B EM simülasyonu veya VNA ölçümü ister. Boşaltmasız
hâli 10.7 ohm ve 12 dB idi, yani boşaltma her hâlükârda büyük bir iyileştirmedir;
doğrulama, iyileştirmenin ne kadar olduğunu belirlemek içindir.
## Anten şartı
Kart **aktif anten gerektirir**. MAX2771'in dahili LNA'ları kapalıdır ve mixer
girişi doğrudan sürülür; bu girişin gürültü figürü **10.3 dB**'dir. Yaklaşık
25 dB kazançlı ve 1.5 dB NF'li bir aktif antenle sistem NF'i ~1.65 dB olur.
**Pasif antenle sistem NF'i 12 dB'in üzerine çıkar ve alıcı kullanışsızdır.**
J5 ile anten beslemesi OFF / 3.3 V / 5 V seçilir; U27 akımı 100 mA ile sınırlar.
+20 -4
View File
@@ -23,6 +23,14 @@ footprintleridir ve varsayılan olarak DNP'dir. Böylece VNA ölçümünden sonr
pi-match kurulabilir. MAX2771'in LNAIN_HI/LNAIN_LO ve LNA çıkışları bağlı pi-match kurulabilir. MAX2771'in LNAIN_HI/LNAIN_LO ve LNA çıkışları bağlı
değildir; register 0 her iki LNA'yı kapatır ve yüksek-band mixer girişi seçilir. değildir; register 0 her iki LNA'yı kapatır ve yüksek-band mixer girişi seçilir.
MAX2771'in dahili LNA'ları kapalı olduğu için mixer girişi ilk aktif kattır ve
NF'i 10.3 dB'dir. Bu yüzden **kart aktif anten gerektirir**: ~25 dB kazanç ve
~1.5 dB NF'li bir antenle sistem NF'i ~1.65 dB olur, pasif antenle 12 dB'in
üzerine çıkar. FL1 SAW'ı da bu yüzden gereklidir; mixer out-of-band IIP3'ü
-9 dBm olduğundan, filtresiz bir aktif anten yakındaki bir telefonun sinyalini
mixer'i tıkayacak seviyeye yükseltir. SAW hücresel ve WiFi bantlarında 35-45 dB
bastırma verir ve sistem NF'ine yalnızca ~0.15 dB ekler.
Aktif anten bias yolu: Aktif anten bias yolu:
```text ```text
@@ -147,11 +155,19 @@ tarafında 10 kohm idle pull-up vardır.
## USB-C güç ve veri ## USB-C güç ve veri
- J1 CC1/CC2 -> U5 TUSB320LAI; PORT=GND ile UFP/sink. Üç konnektör birbirinden bağımsızdır: `J7` yalnızca USB-C güç girişi, `J3`
yalnızca harici 5 V güç girişi, `J1` ise yalnızca JTAG/UART debug portudur.
J1 hiçbir zaman SYS5'i beslemez; bu yüzden J1'e bağlı bir host, kartın 5 V
gücüne back-feed edemez.
- J1 CC1/CC2 -> U5 TUSB320LAI; PORT=GND ile UFP/sink; OUT1/OUT2 kullanılmıyor.
- J1 D+/D− -> U6 TPD2EUSB30A -> R63/R64 27 ohm -> U2 FT4232H. - J1 D+/D− -> U6 TPD2EUSB30A -> R63/R64 27 ohm -> U2 FT4232H.
- J1 VBUS -> U7 TPS259470A -> `USB5_PROT` -> U9 TPS2121 -> `SYS5`. - J1 VBUS -> yalnızca yerel algılama: 900 kohm bölücüyle U5 `VBUS_DET`,
4.7 k/10 k bölücüyle self-powered FT4232H `RESET_N`.
- J7 CC1/CC2 -> U35 TUSB320LAI; PORT=GND ile UFP/sink.
- J7 VBUS -> U7 TPS259470A -> `USB5_PROT` -> U9 TPS2121 -> `SYS5`.
- Harici J3 5 V -> U8 TPS259470A -> `EXT5_PROT` -> U9 TPS2121 -> `SYS5`. - Harici J3 5 V -> U8 TPS259470A -> `EXT5_PROT` -> U9 TPS2121 -> `SYS5`.
- U10/U11, USB 3 A ilanı veya seçilmiş harici kaynak yoksa ana FPGA güç - U10/U11, J7 3 A ilanı veya seçilmiş harici kaynak yoksa ana FPGA güç
converterlarını kapalı tutar. converterlarını kapalı tutar.
## Test noktaları ## Test noktaları
@@ -161,7 +177,7 @@ tarafında 10 kohm idle pull-up vardır.
| TP1 | `U7_FLT` | USB eFuse fault | | TP1 | `U7_FLT` | USB eFuse fault |
| TP2 | `U8_FLT` | Harici giriş eFuse fault | | TP2 | `U8_FLT` | Harici giriş eFuse fault |
| TP3 | `ANT_BIAS_OC` | Aktif anten over-current | | TP3 | `ANT_BIAS_OC` | Aktif anten over-current |
| TP4 | `VBUS_RAW` | USB VBUS | | TP4 | `VBUS_RAW` | J7 USB-C güç girişi VBUS'ı |
| TP5 | `EXT5_RAW` | Harici 5 V giriş | | TP5 | `EXT5_RAW` | Harici 5 V giriş |
| TP6 | `SYS5` | Seçilmiş sistem 5 V | | TP6 | `SYS5` | Seçilmiş sistem 5 V |
| TP7 | `VCC3V3_AUX` | Her zaman açık yardımcı 3.3 V | | TP7 | `VCC3V3_AUX` | Her zaman açık yardımcı 3.3 V |
+64 -8
View File
@@ -2,21 +2,77 @@
## Giriş yolu ## Giriş yolu
Kartta üç ayrı USB-C/güç konnektörü vardır ve hiçbiri birden fazla rolü
paylaşmaz: `J7` yalnızca USB-C güç girişidir, `J3` yalnızca harici 5 V güç
girişidir, `J1` ise yalnızca JTAG/UART debug portudur ve SYS5'i hiçbir zaman
beslemez.
```text ```text
USB-C VBUS -> U7 TPS259470A (2.69 A typ) -> USB5_PROT --+ J7 USB-C VBUS -> U7 TPS259470A (2.69 A typ) -> USB5_PROT --+
+-> U9 TPS2121 -> SYS5 +-> U9 TPS2121 -> SYS5
EXT_5V ----> U8 TPS259470A (4.04 A typ) -> EXT5_PROT --+ J3 EXT_5V ----> U8 TPS259470A (4.04 A typ) -> EXT5_PROT --+
J1 USB-C VBUS -> yalnızca yerel algılama (U5 VBUS_DET, FTDI_RESET_N bölücüsü);
SYS5'e veya herhangi bir eFuse'a bağlı değil.
``` ```
Her iki eFuse yaklaşık 4.0 V UV ve 5.8 V OV eşiğine, kontrollü output slew'a, Her iki eFuse yaklaşık 4.0 V UV (232k/100k) ve 5.8 V OV (383k/100k) eşiğine,
fault test noktasına ve true reverse-current blocking'e sahiptir. U9 4.5 A kontrollü output slew'a, fault test noktasına ve true reverse-current
limitli power mux'tur; yakın geçerli girişlerde harici 5 V yolu önceliklidir. blocking'e sahiptir. Akım limitleri: J7 USB-C yolu 2.69 A (R15 1.24k), J3
harici yolu 4.04 A (R20 825R). U9 4.5 A limitli power mux'tur (R25 22.1k);
kendi OV eşiği 5.69 V'tur (442k/100k).
USB yolunda TUSB320 CC ilanı U10 tarafından decode edilir. `MAIN_EN`, yalnızca **J7 ve J3 aynı anda bağlı olsa bile SYS5 her zaman tek bir kaynaktan
USB kaynağı 3 A ilan ettiğinde veya harici giriş gerçekten U9 tarafından beslenir, ikisi asla toplanmaz.** Bunu iki bağımsız engel garanti eder: her
seçildiğinde yükselir. `VCC3V3_AUX` ise U12 üzerinden SYS5 var olduğu sürece girişin kendi TPS259470A eFuse'unda true reverse-current blocking vardır, ve
TPS2121 sırt sırta FET'lerle her an yalnızca bir girişi OUT'a bağlar.
### Öncelik hangi pinden geliyor
`PR1` GND'ye bağlıdır ve bu, TPS2121'de priority modunu **kapatır** (veri
sayfası: "Connect to GND if not required"). Önceliği belirleyen pin `CP2`'dir
ve `VCC3V3_AUX`'a bağlıdır. Tablo 9-3'e göre `CP2 >= VREF` ve `PR1 < VREF`
iken çıkış **IN2**'dir, yani J3 harici girişi.
`CP2` mux'un kendi çıkışından türeyen bir raydan beslendiği için davranış iki
fazlıdır:
| Faz | CP2 | Seçim kuralı |
|---|---|---|
| Soğuk açılış (VCC3V3_AUX henüz yok) | 0 V | VCOMP: yüksek gerilimli giriş kazanır, eşitlikte IN2 |
| VCC3V3_AUX ayakta | 3.3 V | IN2 (harici 5 V) koşulsuz kazanır |
Yani ikisi de takılıyken ve soğuk açılışta USB-C daha yüksekse, VCC3V3_AUX
geldiği anda harici girişe bir switchover olur. TPS2121 tam olarak bunun için
tasarlanmıştır (seamless, tSW 100 us) ve SYS5'te 138 uF bulk vardır, dolayısıyla
zararsızdır. Harici girişin ilk andan itibaren koşulsuz kazanması istenirse
`CP2` `VCC3V3_AUX` yerine `EXT5_PROT`'tan sürülmelidir.
### ST pull-up (R108)
TPS2121 `ST` açık-drenç bir durum çıkışıdır ve veri sayfasının Recommended
Operating Conditions bölümünde `RST` = 6-20 kohm harici pull-up ister. `ST`
yalnızca IN2 çıkışı sürerken aktif olarak low'a çekilir; IN1 (USB-C) sürerken
yüksek empedanstır. U10'un ikinci kapısı `MUX_ST`'yi ters çevirip
`EXT_SELECTED` üretir ve `MAIN_EN = USB_3A_OK OR EXT_SELECTED` core buck'ı
açar. Pull-up olmadan USB-C ile beslenirken bu kapı girişi havada kalır; low'a
kayarsa TUSB320 ne ilan etmiş olursa olsun MAIN_EN kalkar ve kart 500 mA'lik
bir portta core rayını açmaya çalışır. `R108` 10k bunu VCC3V3_AUX'a çeker.
USB güç yolunda TUSB320 (U35, J7 üzerinde) CC ilanını decode eder ve
`TUSB_OUT1`/`TUSB_OUT2` üzerinden U10'a bildirir. `MAIN_EN`, yalnızca J7 3 A
ilan ettiğinde veya harici giriş gerçekten U9 tarafından seçildiğinde
yükselir. `VCC3V3_AUX` ise U12 üzerinden SYS5 var olduğu sürece
supervisor/debug mantığını besler. supervisor/debug mantığını besler.
J1'in kendi TUSB320'si (U5) yalnızca CC Rd terminasyonu ve UFP algılaması
için kalır; onun OUT1/OUT2 çıkışları artık hiçbir mantığı sürmez, çünkü J1
gücü hiçbir zaman SYS5'e taşımaz. J1'in VBUS pini yalnızca iki yerel bölücüyü
besler: U5'in `VBUS_DET` girişi ve FT4232H'nin self-powered
`FTDI_RESET_N` algılaması. Böylece FT4232H, J1'e gerçek bir USB host
takılmadığı sürece resette kalır; bu davranış J7 veya J3 üzerinden kart
güçlendirilmiş olsa bile değişmez.
## Ray üretimi ## Ray üretimi
| Sıra | Ray | Regülatör | Nominal kapasite | Yükler | | Sıra | Ray | Regülatör | Nominal kapasite | Yükler |
+1
View File
@@ -38,3 +38,4 @@ Script aşağıdaki koşulları doğrular:
Hard JTAG yalnızca kart üzerindeki FT4232H interface 1 üzerinden erişilir; Hard JTAG yalnızca kart üzerindeki FT4232H interface 1 üzerinden erişilir;
harici ARM/JTAG headerı yoktur. `FPGA_CRESET_N`, açık-drenç power/FTDI/harici harici ARM/JTAG headerı yoktur. `FPGA_CRESET_N`, açık-drenç power/FTDI/harici
reset fan-in'i olarak belgelenmiştir. reset fan-in'i olarak belgelenmiştir.
- [Üretim ve kontrollü empedans şartnamesi](FABRICATION.md)
+87 -11
View File
@@ -8,14 +8,17 @@
- `board.kicad_dru`: üreticiye ve kontrollü empedansa özgü DRC kuralları. - `board.kicad_dru`: üreticiye ve kontrollü empedansa özgü DRC kuralları.
- `board.kicad_sym` ve `board.pretty/`: projeye özel sembol ve footprintler. - `board.kicad_sym` ve `board.pretty/`: projeye özel sembol ve footprintler.
`board_prerouted`, `board_plane_stitched` ve eski `board_power_routed` dosyaları `board_prerouted`, `board_plane_stitched`, `board_power_routed`,
ara çalışma ürünleridir. Güncel reset/netlist revizyonundan önce oluşturulmuş `board_power_local` ve `board_routed` dosyaları scriptlerle yeniden üretilen
routing dosyaları yeni tasarım için kaynak kabul edilmez. ara çalışma ürünleridir; kalıcı kararların kaynağı ilgili scriptler ve
`board.kicad_pcb` dosyasıdır.
## Güç mimarisi ## Güç mimarisi
1. USB-C VBUS veya harici 5 V koruma ve power-path devresinden sonra `SYS5` 1. Dedike USB-C güç girişi J7'nin VBUS'ı veya harici 5 V girişi J3, koruma ve
rayını oluşturur. power-path devresinden sonra `SYS5` rayını oluşturur; ikisi aynı anda
bağlıysa TPS2121 yalnızca birini seçer, asla toplanmaz. JTAG/UART debug
portu J1 bu yola hiç dahil değildir ve SYS5'i beslemez.
2. TPS6287B15, FPGA core/VCCA/VDD_SOC için 0.95 V üretir. 2. TPS6287B15, FPGA core/VCCA/VDD_SOC için 0.95 V üretir.
3. `CORE_PG`, LM3880 sıralayıcıyı başlatır. 3. `CORE_PG`, LM3880 sıralayıcıyı başlatır.
4. LM3880 önce TPS62867 1.8 V rayını, ardından TPS62867 3.3 V rayını etkinleştirir. 4. LM3880 önce TPS62867 1.8 V rayını, ardından TPS62867 3.3 V rayını etkinleştirir.
@@ -82,16 +85,89 @@ yoktur.
## RF ve yüksek hızlı routing kuralları ## RF ve yüksek hızlı routing kuralları
- RF katı üst katmanda, JLC08161H-3313 stackup için 0.164 mm genişlik ve - RF katı üst katmanda, JLC08161H-3313 stackup için 0.164 mm genişlik ve
0.200 mm coplanar GND aralığıyla 50 ohm CPWG olarak tasarlanır. 0.200 mm coplanar GND aralığıyla 50 ohm CPWG olarak tasarlanır. Kontrollü
empedans siparişinde bu geometri üreticinin kendi hesabıyla doğrulanmalıdır.
- J2 edge-launch pad'inin altında `RF_LAUNCH_RELIEF` ile L2/L3 boşaltılır.
Konnektörün 1.5 mm genişliğindeki merkez pad'i, 0.0994 mm altındaki dolu
L2_GND üzerinde 50 ohm değil **10.7 ohm**'luk bir hat parçasıdır; plane
boşaltılmadan bu launch tek başına 12 dB return loss ile bütün RF yolunun en
kötü süreksizliğidir. Referans L4_PWR'a (VCC3V0_RF, AC ground) inince launch
~37 ohm ve ~30 dB olur. Boşaltma x=172.35'te, pad bitip 0.164 mm hat
başladığı yerde durur; hat kendi L2 referansını korur.
- D1 ESD klemp doğrudan hattın üzerindedir. 1.34 mm'lik bir stub ucunda
0.3 pF, L1'de -j228 ohm görünüp 3.4 dB return loss'a mal oluyordu.
- Launch boşaltmasının ~37 ohm / 30 dB değeri 2B yarı-statik bir tahmindir.
Konnektör geçişi gerçek bir 3B yapıdır; kesin değer için 3B EM veya VNA
gerekir (bkz. `../docs/FABRICATION.md`). Boşaltmasız hâli 10.7 ohm / 12 dB
olduğu için iyileştirme her hâlükârda büyüktür.
- L4 bias-tee choke'unun SRF'i (2.1 GHz) 1575 MHz'e görece yakındır, ama bu
yararlıdır: 1575 MHz hâlâ SRF'in altındadır, choke endüktiftir ve rezonansa
yakınlık |Z|'yi 554 ohm'dan ~1270 ohm'a çıkarır. L ve SRF'te +/-%10 tolerans
ile |Z| > 900 ohm ve return loss > 30 dB kalır; insertion loss 0.003 dB'in
altındadır. Tek gerçek risk SRF'in 1575 MHz'in altına düşmesidir ki bu
parçada 2.1 GHz nominalden çok uzaktır.
- **Kart aktif anten gerektirir.** Dahili LNA'lar kapalı olduğu için mixer
girişinin NF'i 10.3 dB'dir. ~25 dB kazançlı aktif antenle sistem NF'i
~1.65 dB; pasif antenle 12 dB'in üzerine çıkar ve alıcı kullanışsız olur.
- SMA, ESD, bias-tee, SAW ve MAX2771 MIXIN_HI arasındaki RF güzergâhında dijital - SMA, ESD, bias-tee, SAW ve MAX2771 MIXIN_HI arasındaki RF güzergâhında dijital
route veya via kullanılmaz. route veya via kullanılmaz.
- USB2 diferansiyel çift 0.135 mm hat ve 0.150 mm coupled gap kuralındadır. - USB2 diferansiyel çift 0.135 mm hat ve 0.150 mm coupled gap kuralındadır.
- RF ve USB geometrisi `scripts/finalize_routing.py` tarafından deterministik - RF ve USB geometrisi `scripts/finalize_routing.py` tarafından deterministik
olarak kurulur; Freerouter çıktısındaki kritik hatlar kullanılmaz. olarak kurulur; bu netler autorouter'a hiç verilmez. Yerlerini routing
- L2 ve L7 kesintisiz GND referans katmanlarıdır. L4 bölünmüş DC güç katmanıdır. boyunca `TEMP_RF_LINE` ve `TEMP_USB_*` koridor rule area'ları tutar; bunlar
F.Cu dahil bütün bakır katmanlarını kapsar ve finalde silinir.
- RF hattının koridorunun altında kalıcı bir `RF_NO_DIGITAL` rule area'sı
vardır (L3, L5, L6, B.Cu). Kendisi bakır yasaklamaz; `board.kicad_dru`
içindeki kural POWER dışı net sınıflarına track ve via yasağı koyar. Böylece
ground stitching ve MAX2771'in kendi besleme inişleri geçebilirken, RF
hattının altına veya hemen yanına sonradan elle bir digital hat çekilmesi DRC
hatası olur.
- Alan (152, 36)-(174.5, 43.7) koridorudur; `generate_pcb.py`'nin `TEMP_RF_LINE`
olarak ayırdığı koridorun aynısı. RF bakırı y <= 42.6'da kaldığı için 1.1 mm
açık kalır. Alan bilerek MAX2771'in dijital pin sıralarında (y = 44.5 .. 47.4)
durur: o pinlerin FPGA'ya ulaşması gerekir ve alan onların üstüne
uzatıldığında hepsi yalnız F.Cu'ya sıkışıyor, sığmıyordu. RF dönüş yolu zaten
bu alan tarafından değil, üzerinde hiç route edilmeyen L2/L7 GND plane'leri
tarafından garanti edilir.
- L2 ve L7 kesintisiz GND referans katmanlarıdır ve hiçbir şey route edilmez;
dört routing katmanının her birinin komşusunda bir tanesi bulunur.
- Hiçbir ray tek başına bir katman harcamaz. DC güç L4 ve L6'ya bölünmüş zone
adaları olarak dağıtılır: L4 çekirdek, 5 V giriş, always-on yardımcı ray ve
dört adet 3.0 V ön uç rayını; L6 iki büyük FPGA I/O rayını (`VCC3V3` zemin,
`VCC1V8` kendi yüklerinin üstünde oyulmuş) taşır.
- Bu sayede F.Cu, L3, L5 ve B.Cu dört sinyal katmanı olarak kalır. Ti375'in
fanout'u üç iç/alt katmana (L3, L5, B.Cu) dağıtılır, F.Cu ise BGA escape'i ve
RF ön ucunu taşır.
- Plane'i olmayan küçük raylar ve plane adasının dışında kalan güç pad'leri
`scripts/route_local_power.py` tarafından kilitli, 45 derecelik kısa yollarla
tamamlanır. Bu iş autorouter'a bırakılmaz.
- Ti375 escape alanı içinde bakır, kartın geri kalanının zaten kullandığı
0.10 mm clearance'ta çalışır (`Ti375 escape field copper clearance` kuralı,
her iki nesne de U1 courtyard'ının içindeyken). 0.8 mm pitch, iki komşu
0.45 mm dogbone via arasında 0.35 mm bırakır; CLOCK sınıfının 0.15 mm'si
orada bir clock balını paketten hiç çıkaramaz. Clock trunk, courtyard'ın
dışına çıkar çıkmaz 0.15 mm genişlik ve clearance'ına döner.
- Escape alanının içinde kalan kısa güç inişleri 0.10 mm'dir; en kötü hâlde
(VCC3V0_DIG bank besleme balı) uçtan uca yaklaşık 150 mOhm eder, 100 mA'de
15 mV. Bu balların yanında yerel decoupling vardır.
## Güç dağıtımı kuralı
Güç raylarının hiçbiri autorouter'a verilmez. Sırasıyla plane, plane stitching
ve `route_local_power.py` kullanılır; ancak plane'i olmayan küçük raylar
(`FTDI_1V8`, `FTDI_VPHY`, `FTDI_VPLL`, `VBUS_RAW`, `USB5_PROT`, `EXT5_PROT`,
`ANT_BIAS_SW`) `--extra-nets` ile sinyal katına dahil edilir. Bunun nedeni
Specctra DSN'inin plane içermemesidir: `GND` veya `VCC3V3` autorouter'a
verilseydi, plane'in zaten birleştirdiği yüzlerce pad'i trace ile bağlamaya
çalışırdı.
`board.kicad_pcb` üzerindeki `TEMP_` rule area'ları yalnızca routing süresince
vardır ve `finalize_routing.py` tarafından silinir. RF bölgesindeki GND pad'leri
ile USB koridorundaki güç pad'leri de bu nedenle ancak final aşamada bağlanır.
## Üretime çıkış şartı ## Üretime çıkış şartı
Bu revizyonda placement kontrolleri geçmiştir fakat `board.kicad_pcb` üzerinde Routing akışı tamamlandıktan sonra zone'lar doldurulmalı; ERC, DRC, schematic
track yoktur. Üretim öncesinde routing akışı tamamlanmalı; zone'lar doldurulmalı; parity, Gerber ve drill incelemesi tekrar yapılmalıdır. Final kartta açık
ERC, DRC, schematic parity, Gerber ve drill incelemesi tekrar yapılmalıdır. bağlantı, short, clearance ve isolated copper kabul edilmez; RF uzunlukları,
USB eşleşmesi, güç loopları ve return path görsel olarak da incelenmelidir.
+95
View File
@@ -5100,4 +5100,99 @@ X A 2 700 0 200 L 30 30 1 1 P
ENDDRAW ENDDRAW
ENDDEF ENDDEF
# #
# BOARD_J7
#
DEF BOARD_J7 J 0 30 Y Y 1 L N
F0 "J7" 0 200 50 H V C CNN
F1 "USB-C power input" 0 100 50 H V C CNN
F2 "Connector_USB:USB_C_Receptacle_GCT_USB4105-xx-A_16P_TopMnt_Horizontal" 0 -100 50 H I C CNN
F3 "" 0 0 50 H I C CNN
DRAW
S -500 500 500 -500 1 1 10 f
X GND A1 -700 400 200 R 30 30 1 1 P
X VBUS A4 700 350 200 L 30 30 1 1 P
X CC1 A5 -700 300 200 R 30 30 1 1 P
X D A6 700 250 200 L 30 30 1 1 P
X D A7 -700 200 200 R 30 30 1 1 P
X SBU1 A8 700 150 200 L 30 30 1 1 P
X VBUS A9 -700 100 200 R 30 30 1 1 P
X GND A12 700 50 200 L 30 30 1 1 P
X GND B1 -700 0 200 R 30 30 1 1 P
X VBUS B4 700 -50 200 L 30 30 1 1 P
X CC2 B5 -700 -100 200 R 30 30 1 1 P
X D B6 700 -150 200 L 30 30 1 1 P
X D B7 -700 -200 200 R 30 30 1 1 P
X SBU2 B8 700 -250 200 L 30 30 1 1 P
X VBUS B9 -700 -300 200 R 30 30 1 1 P
X GND B12 700 -350 200 L 30 30 1 1 P
X SHIELD SH -700 -400 200 R 30 30 1 1 P
ENDDRAW
ENDDEF
#
# BOARD_U35
#
DEF BOARD_U35 U 0 30 Y Y 1 L N
F0 "U35" 0 200 50 H V C CNN
F1 "TUSB320LAI" 0 100 50 H V C CNN
F2 "Package_DFN_QFN:Texas_X2QFN-12_1.6x1.6mm_P0.4mm" 0 -100 50 H I C CNN
F3 "" 0 0 50 H I C CNN
DRAW
S -500 350 500 -350 1 1 10 f
X CC1 1 -700 250 200 R 30 30 1 1 B
X CC2 2 700 250 200 L 30 30 1 1 B
X PORT 3 -700 150 200 R 30 30 1 1 I
X VBUS_DET 4 700 150 200 L 30 30 1 1 I
X ADDR 5 -700 50 200 R 30 30 1 1 P
X OUT3 6 700 50 200 L 30 30 1 1 O
X OUT1 7 -700 -50 200 R 30 30 1 1 O
X OUT2 8 700 -50 200 L 30 30 1 1 O
X ID 9 -700 -150 200 R 30 30 1 1 O
X GND 10 700 -150 200 L 30 30 1 1 W
X EN_N 11 -700 -250 200 R 30 30 1 1 I
X VDD 12 700 -250 200 L 30 30 1 1 W
ENDDRAW
ENDDEF
#
# BOARD_R107
#
DEF BOARD_R107 R 0 30 Y Y 1 L N
F0 "R107" 0 200 50 H V C CNN
F1 "900K" 0 100 50 H V C CNN
F2 "Resistor_SMD:R_0402_1005Metric" 0 -100 50 H I C CNN
F3 "" 0 0 50 H I C CNN
DRAW
S -500 150 500 -150 1 1 10 f
X 1 1 -700 0 200 R 30 30 1 1 P
X 2 2 700 0 200 L 30 30 1 1 P
ENDDRAW
ENDDEF
#
# BOARD_C129
#
DEF BOARD_C129 C 0 30 Y Y 1 L N
F0 "C129" 0 200 50 H V C CNN
F1 "100nF" 0 100 50 H V C CNN
F2 "Capacitor_SMD:C_0402_1005Metric" 0 -100 50 H I C CNN
F3 "" 0 0 50 H I C CNN
DRAW
S -500 150 500 -150 1 1 10 f
X 1 1 -700 0 200 R 30 30 1 1 P
X 2 2 700 0 200 L 30 30 1 1 P
ENDDRAW
ENDDEF
#
# BOARD_R108
#
DEF BOARD_R108 R 0 30 Y Y 1 L N
F0 "R108" 0 200 50 H V C CNN
F1 "10K" 0 100 50 H V C CNN
F2 "Resistor_SMD:R_0402_1005Metric" 0 -100 50 H I C CNN
F3 "" 0 0 50 H I C CNN
DRAW
S -500 150 500 -150 1 1 10 f
X 1 1 -700 0 200 R 30 30 1 1 P
X 2 2 700 0 200 L 30 30 1 1 P
ENDDRAW
ENDDEF
#
#End Library #End Library
+45 -9
View File
@@ -17,15 +17,19 @@
(condition "A.Reference == 'U13' && B.Reference == 'U13'") (condition "A.Reference == 'U13' && B.Reference == 'U13'")
(constraint clearance (min 0.10mm))) (constraint clearance (min 0.10mm)))
# A 0.45-mm dogbone via at the centre of four 0.8-mm-pitch BGA lands has # Inside the Ti375 escape field the 0.8-mm ball pitch fixes the geometry: a
# 0.1407-mm geometric clearance. This local rule is still well above the # 0.45-mm dogbone via between four lands leaves 0.1407 mm, and a 0.12-mm trace
# selected fabricator's 0.10-mm copper minimum; clocks retain 0.15 mm outside # threading between two lands leaves 0.14 mm only if it stays exactly centred.
# the U1 escape field. # This field therefore runs at the fabricator's 0.10-mm copper minimum, which
(rule "Ti375 0.8-mm BGA dogbone clearance" # is what the Default class uses across the rest of the board anyway. The rule
# only fires when one of the two objects belongs to U1, so it relaxes copper
# against the balls and nothing else: two tracks crossing the field still owe
# each other their own net classes' clearance, CLOCK's 0.15 mm included.
(rule "Ti375 0.8-mm BGA escape clearance"
(condition "A.Reference == 'U1' || B.Reference == 'U1'") (condition "A.Reference == 'U1' || B.Reference == 'U1'")
(constraint clearance (min 0.14mm))) (constraint clearance (min 0.10mm)))
# JLC08161H-3313 L1/L2 geometry. Net-class widths are routing defaults only; # JLC08161H-3313 L1/L2 geometry (3313 prepreg, 0.0994 mm, er 4.1). Net-class widths are routing defaults only;
# these rules make the controlled-impedance geometries actual DRC limits. # these rules make the controlled-impedance geometries actual DRC limits.
(rule "GNSS 50-ohm CPWG geometry" (rule "GNSS 50-ohm CPWG geometry"
(condition "A.NetClass == 'RF50_CPWG'") (condition "A.NetClass == 'RF50_CPWG'")
@@ -39,10 +43,42 @@
(condition "A.NetClass == 'USB90_DIFF'") (condition "A.NetClass == 'USB90_DIFF'")
(constraint track_width (min 0.135mm) (opt 0.135mm) (max 0.135mm))) (constraint track_width (min 0.135mm) (opt 0.135mm) (max 0.135mm)))
# The TPD2EUSB30A clamp is a SOT-23 whose ground pad sits between its two
# channel pads, so the pair has to open up for about 1.5 mm to pass it. The
# 0.90-mm coupled limit still applies over the whole rest of the run.
(rule "USB2 coupled gap" (rule "USB2 coupled gap"
(condition "A.NetClass == 'USB90_DIFF' && AB.isCoupledDiffPair()") (condition "A.NetClass == 'USB90_DIFF' && AB.isCoupledDiffPair() && !A.intersectsArea('USB_ESD_BREAKOUT')")
(constraint diff_pair_gap (min 0.15mm) (opt 0.15mm) (max 0.15mm))) (constraint diff_pair_gap (min 0.15mm) (opt 0.15mm) (max 0.90mm)))
# The 50-ohm line owns the band it runs in on every layer under F.Cu, so no
# digital route can pass beneath or immediately beside it. Ground stitching
# and the MAX2771's own supply drops still have to reach it, so the disallow is
# scoped to the signal classes rather than made a plain keepout. The RF line
# itself is on F.Cu, which the area does not cover, and the L2/L7 GND planes
# stay solid because nothing routes on them at all -- the actual RF return path
# is guaranteed by those planes, not by this area.
#
# The area covers y <= 43.7, which is the same corridor generate_pcb.py
# reserves as TEMP_RF_LINE and clears the RF copper (y <= 42.6) by 1.1 mm. It
# deliberately stops short of the MAX2771's digital pin rows at y = 44.5 ..
# 47.4: those pins have to reach the FPGA, and with the area extended over them
# every one of them was forced onto F.Cu alone, which does not fit.
(rule "No digital copper under the GNSS front end"
(condition "A.intersectsArea('RF_NO_DIGITAL') && A.NetClass != 'POWER'")
(constraint disallow track via))
(rule "Clock trunk width outside Ti375 escape" (rule "Clock trunk width outside Ti375 escape"
(condition "A.NetClass == 'CLOCK' && A.Type == 'Track' && !A.insideCourtyard('U1')") (condition "A.NetClass == 'CLOCK' && A.Type == 'Track' && !A.insideCourtyard('U1')")
(constraint track_width (min 0.15mm))) (constraint track_width (min 0.15mm)))
# A 0.8-mm ball pitch leaves a 0.35-mm channel between two adjacent 0.45-mm
# dogbone vias. A 0.10-mm trace fits that channel at the escape field's
# 0.10-mm copper minimum but not at CLOCK's 0.15 mm, so a clock ball could not
# be escaped from the package at all. Inside the field -- and only where BOTH
# objects are inside it -- copper therefore runs at the same 0.10 mm the rest
# of this field already uses. The clock trunk keeps its 0.15 mm the moment it
# leaves U1's courtyard, which is where coupling to neighbours actually costs
# something.
(rule "Ti375 escape field copper clearance"
(condition "A.insideCourtyard('U1') && B.insideCourtyard('U1')")
(constraint clearance (min 0.10mm)))
+57289 -54619
View File
File diff suppressed because it is too large Load Diff
+4760 -4198
View File
File diff suppressed because it is too large Load Diff
+288 -3
View File
@@ -2426,7 +2426,7 @@
(property "Manufacturer" "GCT" (id 5) (at 0.0 0.0 0) (property "Manufacturer" "GCT" (id 5) (at 0.0 0.0 0)
(effects (font (size 0.8 0.8)) hide) (effects (font (size 0.8 0.8)) hide)
) )
(property "Description" "USB 2.0 Type-C receptacle, UFP/sink" (id 6) (at 0.0 0.0 0) (property "Description" "USB 2.0 Type-C receptacle, UFP/sink, debug data only" (id 6) (at 0.0 0.0 0)
(effects (font (size 0.8 0.8)) hide) (effects (font (size 0.8 0.8)) hide)
) )
(symbol "BOARD_J1_1_1" (symbol "BOARD_J1_1_1"
@@ -2523,7 +2523,7 @@
(property "Manufacturer" "Texas Instruments" (id 5) (at 0.0 0.0 0) (property "Manufacturer" "Texas Instruments" (id 5) (at 0.0 0.0 0)
(effects (font (size 0.8 0.8)) hide) (effects (font (size 0.8 0.8)) hide)
) )
(property "Description" "USB Type-C port controller in UFP GPIO mode; integrated Rd on CC1/CC2" (id 6) (at 0.0 0.0 0) (property "Description" "USB Type-C port controller in UFP GPIO mode; integrated Rd on CC1/CC2; current-advertisement outputs unused since this port never sources SYS5" (id 6) (at 0.0 0.0 0)
(effects (font (size 0.8 0.8)) hide) (effects (font (size 0.8 0.8)) hide)
) )
(symbol "BOARD_U5_1_1" (symbol "BOARD_U5_1_1"
@@ -3733,7 +3733,7 @@
(property "Manufacturer" "Texas Instruments" (id 5) (at 0.0 0.0 0) (property "Manufacturer" "Texas Instruments" (id 5) (at 0.0 0.0 0)
(effects (font (size 0.8 0.8)) hide) (effects (font (size 0.8 0.8)) hide)
) )
(property "Description" "4.5-A power mux; external input wins near-equal valid 5-V inputs" (id 6) (at 0.0 0.0 0) (property "Description" "4.5-A power mux. PR1=GND disables priority mode; CP2 tied to VCC3V3_AUX is what selects IN2 (external 5 V) once the aux rail is up" (id 6) (at 0.0 0.0 0)
(effects (font (size 0.8 0.8)) hide) (effects (font (size 0.8 0.8)) hide)
) )
(symbol "BOARD_U9_1_1" (symbol "BOARD_U9_1_1"
@@ -14664,6 +14664,291 @@
) )
) )
) )
(symbol "BOARD_J7" (pin_names (offset 0.508)) (in_bom yes) (on_board yes)
(property "Reference" "J" (id 0) (at 0.0 0.0 0)
(effects (font (size 1.27 1.27)))
)
(property "Value" "USB-C power input" (id 1) (at 0.0 0.0 0)
(effects (font (size 1.27 1.27)))
)
(property "Footprint" "Connector_USB:USB_C_Receptacle_GCT_USB4105-xx-A_16P_TopMnt_Horizontal" (id 2) (at 0.0 0.0 0)
(effects (font (size 1.27 1.27)) hide)
)
(property "Datasheet" "" (id 3) (at 0.0 0.0 0)
(effects (font (size 1.27 1.27)) hide)
)
(property "MPN" "USB4105-GF-A-120" (id 4) (at 0.0 0.0 0)
(effects (font (size 0.8 0.8)) hide)
)
(property "Manufacturer" "GCT" (id 5) (at 0.0 0.0 0)
(effects (font (size 0.8 0.8)) hide)
)
(property "Description" "USB 2.0 Type-C receptacle, UFP/sink, power input only" (id 6) (at 0.0 0.0 0)
(effects (font (size 0.8 0.8)) hide)
)
(symbol "BOARD_J7_1_1"
(rectangle (start -12.7 -12.7) (end 12.7 12.7)
(stroke (width 0.254) (type default))
(fill (type background))
)
(pin passive line (at -17.78 -10.16 0) (length 5.08)
(name "GND" (effects (font (size 0.65 0.65))))
(number "A1" (effects (font (size 0.65 0.65))))
)
(pin passive line (at 17.78 -8.89 180) (length 5.08)
(name "VBUS" (effects (font (size 0.65 0.65))))
(number "A4" (effects (font (size 0.65 0.65))))
)
(pin passive line (at -17.78 -7.62 0) (length 5.08)
(name "CC1" (effects (font (size 0.65 0.65))))
(number "A5" (effects (font (size 0.65 0.65))))
)
(pin passive line (at 17.78 -6.3500000000000005 180) (length 5.08)
(name "D+" (effects (font (size 0.65 0.65))))
(number "A6" (effects (font (size 0.65 0.65))))
)
(pin passive line (at -17.78 -5.08 0) (length 5.08)
(name "D-" (effects (font (size 0.65 0.65))))
(number "A7" (effects (font (size 0.65 0.65))))
)
(pin passive line (at 17.78 -3.8100000000000005 180) (length 5.08)
(name "SBU1" (effects (font (size 0.65 0.65))))
(number "A8" (effects (font (size 0.65 0.65))))
)
(pin passive line (at -17.78 -2.54 0) (length 5.08)
(name "VBUS" (effects (font (size 0.65 0.65))))
(number "A9" (effects (font (size 0.65 0.65))))
)
(pin passive line (at 17.78 -1.2700000000000005 180) (length 5.08)
(name "GND" (effects (font (size 0.65 0.65))))
(number "A12" (effects (font (size 0.65 0.65))))
)
(pin passive line (at -17.78 0.0 0) (length 5.08)
(name "GND" (effects (font (size 0.65 0.65))))
(number "B1" (effects (font (size 0.65 0.65))))
)
(pin passive line (at 17.78 1.2699999999999996 180) (length 5.08)
(name "VBUS" (effects (font (size 0.65 0.65))))
(number "B4" (effects (font (size 0.65 0.65))))
)
(pin passive line (at -17.78 2.539999999999999 0) (length 5.08)
(name "CC2" (effects (font (size 0.65 0.65))))
(number "B5" (effects (font (size 0.65 0.65))))
)
(pin passive line (at 17.78 3.8099999999999987 180) (length 5.08)
(name "D+" (effects (font (size 0.65 0.65))))
(number "B6" (effects (font (size 0.65 0.65))))
)
(pin passive line (at -17.78 5.08 0) (length 5.08)
(name "D-" (effects (font (size 0.65 0.65))))
(number "B7" (effects (font (size 0.65 0.65))))
)
(pin passive line (at 17.78 6.35 180) (length 5.08)
(name "SBU2" (effects (font (size 0.65 0.65))))
(number "B8" (effects (font (size 0.65 0.65))))
)
(pin passive line (at -17.78 7.620000000000001 0) (length 5.08)
(name "VBUS" (effects (font (size 0.65 0.65))))
(number "B9" (effects (font (size 0.65 0.65))))
)
(pin passive line (at 17.78 8.89 180) (length 5.08)
(name "GND" (effects (font (size 0.65 0.65))))
(number "B12" (effects (font (size 0.65 0.65))))
)
(pin passive line (at -17.78 10.16 0) (length 5.08)
(name "SHIELD" (effects (font (size 0.65 0.65))))
(number "SH" (effects (font (size 0.65 0.65))))
)
)
)
(symbol "BOARD_U35" (pin_names (offset 0.508)) (in_bom yes) (on_board yes)
(property "Reference" "U" (id 0) (at 0.0 0.0 0)
(effects (font (size 1.27 1.27)))
)
(property "Value" "TUSB320LAI" (id 1) (at 0.0 0.0 0)
(effects (font (size 1.27 1.27)))
)
(property "Footprint" "Package_DFN_QFN:Texas_X2QFN-12_1.6x1.6mm_P0.4mm" (id 2) (at 0.0 0.0 0)
(effects (font (size 1.27 1.27)) hide)
)
(property "Datasheet" "" (id 3) (at 0.0 0.0 0)
(effects (font (size 1.27 1.27)) hide)
)
(property "MPN" "TUSB320LAIRSW(R)" (id 4) (at 0.0 0.0 0)
(effects (font (size 0.8 0.8)) hide)
)
(property "Manufacturer" "Texas Instruments" (id 5) (at 0.0 0.0 0)
(effects (font (size 0.8 0.8)) hide)
)
(property "Description" "USB Type-C port controller in UFP GPIO mode for the power input; integrated Rd on CC1/CC2; OUT1/OUT2 3-A advertisement drives U10/U11 MAIN_EN gating" (id 6) (at 0.0 0.0 0)
(effects (font (size 0.8 0.8)) hide)
)
(symbol "BOARD_U35_1_1"
(rectangle (start -12.7 -8.89) (end 12.7 8.89)
(stroke (width 0.254) (type default))
(fill (type background))
)
(pin bidirectional line (at -17.78 -6.35 0) (length 5.08)
(name "CC1" (effects (font (size 0.65 0.65))))
(number "1" (effects (font (size 0.65 0.65))))
)
(pin bidirectional line (at 17.78 -6.35 180) (length 5.08)
(name "CC2" (effects (font (size 0.65 0.65))))
(number "2" (effects (font (size 0.65 0.65))))
)
(pin input line (at -17.78 -3.8099999999999996 0) (length 5.08)
(name "PORT" (effects (font (size 0.65 0.65))))
(number "3" (effects (font (size 0.65 0.65))))
)
(pin input line (at 17.78 -3.8099999999999996 180) (length 5.08)
(name "VBUS_DET" (effects (font (size 0.65 0.65))))
(number "4" (effects (font (size 0.65 0.65))))
)
(pin passive line (at -17.78 -1.2699999999999996 0) (length 5.08)
(name "ADDR" (effects (font (size 0.65 0.65))))
(number "5" (effects (font (size 0.65 0.65))))
)
(pin output line (at 17.78 -1.2699999999999996 180) (length 5.08)
(name "OUT3" (effects (font (size 0.65 0.65))))
(number "6" (effects (font (size 0.65 0.65))))
)
(pin output line (at -17.78 1.2700000000000005 0) (length 5.08)
(name "OUT1" (effects (font (size 0.65 0.65))))
(number "7" (effects (font (size 0.65 0.65))))
)
(pin output line (at 17.78 1.2700000000000005 180) (length 5.08)
(name "OUT2" (effects (font (size 0.65 0.65))))
(number "8" (effects (font (size 0.65 0.65))))
)
(pin output line (at -17.78 3.8100000000000005 0) (length 5.08)
(name "ID" (effects (font (size 0.65 0.65))))
(number "9" (effects (font (size 0.65 0.65))))
)
(pin power_in line (at 17.78 3.8100000000000005 180) (length 5.08)
(name "GND" (effects (font (size 0.65 0.65))))
(number "10" (effects (font (size 0.65 0.65))))
)
(pin input line (at -17.78 6.35 0) (length 5.08)
(name "EN_N" (effects (font (size 0.65 0.65))))
(number "11" (effects (font (size 0.65 0.65))))
)
(pin power_in line (at 17.78 6.35 180) (length 5.08)
(name "VDD" (effects (font (size 0.65 0.65))))
(number "12" (effects (font (size 0.65 0.65))))
)
)
)
(symbol "BOARD_R107" (pin_names (offset 0.508)) (in_bom yes) (on_board yes)
(property "Reference" "R" (id 0) (at 0.0 0.0 0)
(effects (font (size 1.27 1.27)))
)
(property "Value" "900K" (id 1) (at 0.0 0.0 0)
(effects (font (size 1.27 1.27)))
)
(property "Footprint" "Resistor_SMD:R_0402_1005Metric" (id 2) (at 0.0 0.0 0)
(effects (font (size 1.27 1.27)) hide)
)
(property "Datasheet" "" (id 3) (at 0.0 0.0 0)
(effects (font (size 1.27 1.27)) hide)
)
(property "MPN" "RC0402FR-07909KL" (id 4) (at 0.0 0.0 0)
(effects (font (size 0.8 0.8)) hide)
)
(property "Manufacturer" "Yageo" (id 5) (at 0.0 0.0 0)
(effects (font (size 0.8 0.8)) hide)
)
(property "Description" "TUSB320 VBUS_DET current limiter" (id 6) (at 0.0 0.0 0)
(effects (font (size 0.8 0.8)) hide)
)
(symbol "BOARD_R107_1_1"
(rectangle (start -12.7 -3.81) (end 12.7 3.81)
(stroke (width 0.254) (type default))
(fill (type background))
)
(pin passive line (at -17.78 0.0 0) (length 5.08)
(name "1" (effects (font (size 0.65 0.65))))
(number "1" (effects (font (size 0.65 0.65))))
)
(pin passive line (at 17.78 0.0 180) (length 5.08)
(name "2" (effects (font (size 0.65 0.65))))
(number "2" (effects (font (size 0.65 0.65))))
)
)
)
(symbol "BOARD_C129" (pin_names (offset 0.508)) (in_bom yes) (on_board yes)
(property "Reference" "C" (id 0) (at 0.0 0.0 0)
(effects (font (size 1.27 1.27)))
)
(property "Value" "100nF" (id 1) (at 0.0 0.0 0)
(effects (font (size 1.27 1.27)))
)
(property "Footprint" "Capacitor_SMD:C_0402_1005Metric" (id 2) (at 0.0 0.0 0)
(effects (font (size 1.27 1.27)) hide)
)
(property "Datasheet" "" (id 3) (at 0.0 0.0 0)
(effects (font (size 1.27 1.27)) hide)
)
(property "MPN" "GRM155R71A104KA01D" (id 4) (at 0.0 0.0 0)
(effects (font (size 0.8 0.8)) hide)
)
(property "Manufacturer" "Murata" (id 5) (at 0.0 0.0 0)
(effects (font (size 0.8 0.8)) hide)
)
(property "Description" "TUSB320 local bypass" (id 6) (at 0.0 0.0 0)
(effects (font (size 0.8 0.8)) hide)
)
(symbol "BOARD_C129_1_1"
(rectangle (start -12.7 -3.81) (end 12.7 3.81)
(stroke (width 0.254) (type default))
(fill (type background))
)
(pin passive line (at -17.78 0.0 0) (length 5.08)
(name "1" (effects (font (size 0.65 0.65))))
(number "1" (effects (font (size 0.65 0.65))))
)
(pin passive line (at 17.78 0.0 180) (length 5.08)
(name "2" (effects (font (size 0.65 0.65))))
(number "2" (effects (font (size 0.65 0.65))))
)
)
)
(symbol "BOARD_R108" (pin_names (offset 0.508)) (in_bom yes) (on_board yes)
(property "Reference" "R" (id 0) (at 0.0 0.0 0)
(effects (font (size 1.27 1.27)))
)
(property "Value" "10K" (id 1) (at 0.0 0.0 0)
(effects (font (size 1.27 1.27)))
)
(property "Footprint" "Resistor_SMD:R_0402_1005Metric" (id 2) (at 0.0 0.0 0)
(effects (font (size 1.27 1.27)) hide)
)
(property "Datasheet" "" (id 3) (at 0.0 0.0 0)
(effects (font (size 1.27 1.27)) hide)
)
(property "MPN" "RC0402FR-0710KL" (id 4) (at 0.0 0.0 0)
(effects (font (size 0.8 0.8)) hide)
)
(property "Manufacturer" "Yageo" (id 5) (at 0.0 0.0 0)
(effects (font (size 0.8 0.8)) hide)
)
(property "Description" "TPS2121 open-drain status pull-up (RST, 6-20 kohm)" (id 6) (at 0.0 0.0 0)
(effects (font (size 0.8 0.8)) hide)
)
(symbol "BOARD_R108_1_1"
(rectangle (start -12.7 -3.81) (end 12.7 3.81)
(stroke (width 0.254) (type default))
(fill (type background))
)
(pin passive line (at -17.78 0.0 0) (length 5.08)
(name "1" (effects (font (size 0.65 0.65))))
(number "1" (effects (font (size 0.65 0.65))))
)
(pin passive line (at 17.78 0.0 180) (length 5.08)
(name "2" (effects (font (size 0.65 0.65))))
(number "2" (effects (font (size 0.65 0.65))))
)
)
)
(symbol "PWR_FLAG" (power) (in_bom no) (on_board no) (symbol "PWR_FLAG" (power) (in_bom no) (on_board no)
(property "Reference" "#FLG" (id 0) (at 0.0 0.0 0) (property "Reference" "#FLG" (id 0) (at 0.0 0.0 0)
(effects (font (size 1.27 1.27))) (effects (font (size 1.27 1.27)))
+84
View File
@@ -0,0 +1,84 @@
(version 1)
# The edge-launch SMA copper must reach the routed board edge by construction.
(rule "J2 edge-launch lands"
(condition "A.Reference == 'J2' || B.Reference == 'J2'")
(constraint edge_clearance (min -0.01mm)))
# The manufacturer USB-C locating holes are 0.194 mm from grounded shell
# lands. This exception is internal to the qualified connector footprint.
(rule "USB-C locating-hole clearance"
(condition "A.Reference == 'J1' && B.Reference == 'J1'")
(constraint hole_clearance (min 0.19mm)))
# RZV0024A is an ultra-dense high-current package. Its adjusted corner lands
# and thermal zones meet the selected JLCPCB 0.10-mm copper rule.
(rule "TPS6287B RZV internal clearance"
(condition "A.Reference == 'U13' && B.Reference == 'U13'")
(constraint clearance (min 0.10mm)))
# Inside the Ti375 escape field the 0.8-mm ball pitch fixes the geometry: a
# 0.45-mm dogbone via between four lands leaves 0.1407 mm, and a 0.12-mm trace
# threading between two lands leaves 0.14 mm only if it stays exactly centred.
# This field therefore runs at the fabricator's 0.10-mm copper minimum, which
# is what the Default class uses across the rest of the board anyway. The rule
# only fires when one of the two objects belongs to U1, so it relaxes copper
# against the balls and nothing else: two tracks crossing the field still owe
# each other their own net classes' clearance, CLOCK's 0.15 mm included.
(rule "Ti375 0.8-mm BGA escape clearance"
(condition "A.Reference == 'U1' || B.Reference == 'U1'")
(constraint clearance (min 0.10mm)))
# JLC08161H-3313 L1/L2 geometry (3313 prepreg, 0.0994 mm, er 4.1). Net-class widths are routing defaults only;
# these rules make the controlled-impedance geometries actual DRC limits.
(rule "GNSS 50-ohm CPWG geometry"
(condition "A.NetClass == 'RF50_CPWG'")
(constraint track_width (min 0.164mm) (opt 0.164mm) (max 0.164mm)))
(rule "GNSS RF stays on top layer"
(condition "A.NetClass == 'RF50_CPWG' && A.Type == 'Track' && A.Layer != 'F.Cu'")
(constraint disallow track))
(rule "USB2 90-ohm differential geometry"
(condition "A.NetClass == 'USB90_DIFF'")
(constraint track_width (min 0.135mm) (opt 0.135mm) (max 0.135mm)))
# The TPD2EUSB30A clamp is a SOT-23 whose ground pad sits between its two
# channel pads, so the pair has to open up for about 1.5 mm to pass it. The
# 0.90-mm coupled limit still applies over the whole rest of the run.
(rule "USB2 coupled gap"
(condition "A.NetClass == 'USB90_DIFF' && AB.isCoupledDiffPair() && !A.intersectsArea('USB_ESD_BREAKOUT')")
(constraint diff_pair_gap (min 0.15mm) (opt 0.15mm) (max 0.90mm)))
# The 50-ohm line owns the band it runs in on every layer under F.Cu, so no
# digital route can pass beneath or immediately beside it. Ground stitching
# and the MAX2771's own supply drops still have to reach it, so the disallow is
# scoped to the signal classes rather than made a plain keepout. The RF line
# itself is on F.Cu, which the area does not cover, and the L2/L7 GND planes
# stay solid because nothing routes on them at all -- the actual RF return path
# is guaranteed by those planes, not by this area.
#
# The area covers y <= 43.7, which is the same corridor generate_pcb.py
# reserves as TEMP_RF_LINE and clears the RF copper (y <= 42.6) by 1.1 mm. It
# deliberately stops short of the MAX2771's digital pin rows at y = 44.5 ..
# 47.4: those pins have to reach the FPGA, and with the area extended over them
# every one of them was forced onto F.Cu alone, which does not fit.
(rule "No digital copper under the GNSS front end"
(condition "A.intersectsArea('RF_NO_DIGITAL') && A.NetClass != 'POWER'")
(constraint disallow track via))
(rule "Clock trunk width outside Ti375 escape"
(condition "A.NetClass == 'CLOCK' && A.Type == 'Track' && !A.insideCourtyard('U1')")
(constraint track_width (min 0.15mm)))
# A 0.8-mm ball pitch leaves a 0.35-mm channel between two adjacent 0.45-mm
# dogbone vias. A 0.10-mm trace fits that channel at the escape field's
# 0.10-mm copper minimum but not at CLOCK's 0.15 mm, so a clock ball could not
# be escaped from the package at all. Inside the field -- and only where BOTH
# objects are inside it -- copper therefore runs at the same 0.10 mm the rest
# of this field already uses. The clock trunk keeps its 0.15 mm the moment it
# leaves U1's courtyard, which is where coupling to neighbours actually costs
# something.
(rule "Ti375 escape field copper clearance"
(condition "A.insideCourtyard('U1') && B.insideCourtyard('U1')")
(constraint clearance (min 0.10mm)))
File diff suppressed because it is too large Load Diff
+582
View File
@@ -0,0 +1,582 @@
{
"board": {
"3dviewports": [],
"design_settings": {
"defaults": {
"apply_defaults_to_fp_barcodes": false,
"apply_defaults_to_fp_dimensions": false,
"apply_defaults_to_fp_fields": false,
"apply_defaults_to_fp_shapes": false,
"apply_defaults_to_fp_text": false,
"board_outline_line_width": 0.05,
"copper_line_width": 0.2,
"copper_text_italic": false,
"copper_text_size_h": 1.5,
"copper_text_size_v": 1.5,
"copper_text_thickness": 0.3,
"copper_text_upright": false,
"courtyard_line_width": 0.05,
"dimension_precision": 4,
"dimension_units": 3,
"dimensions": {
"arrow_length": 1270000,
"extension_offset": 500000,
"keep_text_aligned": true,
"suppress_zeroes": true,
"text_position": 0,
"units_format": 0
},
"fab_line_width": 0.1,
"fab_text_italic": false,
"fab_text_size_h": 1.0,
"fab_text_size_v": 1.0,
"fab_text_thickness": 0.15,
"fab_text_upright": false,
"other_line_width": 0.1,
"other_text_italic": false,
"other_text_size_h": 1.0,
"other_text_size_v": 1.0,
"other_text_thickness": 0.15,
"other_text_upright": false,
"pads": {
"drill": 0.8,
"height": 1.27,
"width": 1.27
},
"silk_line_width": 0.1,
"silk_text_italic": false,
"silk_text_size_h": 1.0,
"silk_text_size_v": 1.0,
"silk_text_thickness": 0.1,
"silk_text_upright": false,
"zones": {
"border_display_style": 2,
"border_hatch_pitch": 0.5,
"corner_radius": 0.0,
"corner_smoothing": 0,
"fill_mode": 0,
"hatch_gap": 1.5,
"hatch_orientation": 0.0,
"hatch_smoothing_level": 0,
"hatch_smoothing_value": 0.1,
"hatch_thickness": 1.0,
"min_clearance": 0.5,
"min_island_area": 10.0,
"min_thickness": 0.25,
"pad_connection": 1,
"remove_islands": 0,
"thermal_relief_gap": 0.5,
"thermal_relief_spoke_width": 0.5
}
},
"diff_pair_dimensions": [],
"drc_exclusions": [],
"meta": {
"version": 2
},
"rule_severities": {
"annular_width": "error",
"clearance": "error",
"connection_width": "warning",
"copper_edge_clearance": "error",
"copper_sliver": "warning",
"courtyards_overlap": "error",
"creepage": "error",
"diff_pair_gap_out_of_range": "error",
"diff_pair_uncoupled_length_too_long": "error",
"drill_out_of_range": "error",
"duplicate_footprints": "warning",
"extra_footprint": "warning",
"footprint": "error",
"footprint_filters_mismatch": "ignore",
"footprint_symbol_field_mismatch": "warning",
"footprint_symbol_mismatch": "warning",
"footprint_type_mismatch": "ignore",
"hole_clearance": "error",
"hole_to_hole": "warning",
"holes_co_located": "warning",
"invalid_outline": "error",
"isolated_copper": "warning",
"item_on_disabled_layer": "error",
"items_not_allowed": "error",
"length_out_of_range": "error",
"lib_footprint_issues": "warning",
"lib_footprint_mismatch": "warning",
"malformed_courtyard": "error",
"microvia_drill_out_of_range": "error",
"mirrored_text_on_front_layer": "warning",
"missing_courtyard": "ignore",
"missing_footprint": "warning",
"missing_tuning_profile": "warning",
"net_conflict": "warning",
"nonmirrored_text_on_back_layer": "warning",
"npth_inside_courtyard": "error",
"padstack": "warning",
"pth_inside_courtyard": "error",
"shorting_items": "error",
"silk_edge_clearance": "warning",
"silk_over_copper": "warning",
"silk_overlap": "warning",
"skew_out_of_range": "error",
"solder_mask_bridge": "error",
"starved_thermal": "error",
"text_height": "warning",
"text_on_edge_cuts": "error",
"text_thickness": "warning",
"through_hole_pad_without_hole": "error",
"too_many_vias": "error",
"track_angle": "error",
"track_dangling": "warning",
"track_not_centered_on_via": "ignore",
"track_on_post_machined_layer": "error",
"track_segment_length": "error",
"track_width": "error",
"tracks_crossing": "error",
"tuning_profile_track_geometries": "ignore",
"unconnected_items": "error",
"unresolved_variable": "error",
"via_dangling": "warning",
"zones_intersect": "error"
},
"rules": {
"max_error": 0.005,
"min_clearance": 0.1,
"min_connection": 0.0,
"min_copper_edge_clearance": 0.25,
"min_groove_width": 0.0,
"min_hole_clearance": 0.19,
"min_hole_to_hole": 0.25,
"min_microvia_diameter": 0.2,
"min_microvia_drill": 0.1,
"min_resolved_spokes": 2,
"min_silk_clearance": 0.0,
"min_text_height": 0.8,
"min_text_thickness": 0.08,
"min_through_hole_diameter": 0.225,
"min_track_width": 0.1,
"min_via_annular_width": 0.1,
"min_via_diameter": 0.45,
"solder_mask_to_copper_clearance": 0.0,
"use_height_for_length_calcs": true
},
"teardrop_options": [
{
"td_onpthpad": true,
"td_onroundshapesonly": false,
"td_onsmdpad": true,
"td_ontrackend": false,
"td_onvia": true
}
],
"teardrop_parameters": [
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_round_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_rect_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_track_end",
"td_width_to_size_filter_ratio": 0.9
}
],
"track_widths": [],
"tuning_pattern_settings": {
"diff_pair_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 1.0
},
"diff_pair_skew_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
},
"single_track_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
}
},
"via_dimensions": [],
"zones_allow_external_fillets": false
},
"ipc2581": {
"bom_rev": "",
"dist": "",
"distpn": "",
"internal_id": "",
"mfg": "",
"mpn": "",
"sch_revision": ""
},
"layer_pairs": [],
"layer_presets": [],
"viewports": []
},
"boards": [],
"component_class_settings": {
"assignments": [],
"meta": {
"version": 0
},
"sheet_component_classes": {
"enabled": false
}
},
"cvpcb": {
"equivalence_files": []
},
"libraries": {
"pinned_footprint_libs": [],
"pinned_symbol_libs": []
},
"meta": {
"filename": "board_routed.kicad_pro",
"version": 3
},
"net_settings": {
"classes": [
{
"bus_width": 12,
"clearance": 0.1,
"diff_pair_gap": 0.15,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.135,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "Default",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 2147483647,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.12,
"tuning_profile": "",
"via_diameter": 0.45,
"via_drill": 0.225,
"wire_width": 6
},
{
"bus_width": 12,
"clearance": 0.1,
"diff_pair_gap": 0.25,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.2,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "BGA_ESCAPE",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": -1,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.1,
"tuning_profile": "",
"via_diameter": 0.45,
"via_drill": 0.225,
"wire_width": 6
},
{
"bus_width": 12,
"clearance": 0.15,
"diff_pair_gap": 0.25,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.2,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "CLOCK",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": -1,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.15,
"tuning_profile": "",
"via_diameter": 0.45,
"via_drill": 0.225,
"wire_width": 6
},
{
"bus_width": 12,
"clearance": 0.1,
"diff_pair_gap": 0.25,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.2,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "POWER",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": -1,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.4,
"tuning_profile": "",
"via_diameter": 0.45,
"via_drill": 0.225,
"wire_width": 6
},
{
"bus_width": 12,
"clearance": 0.2,
"diff_pair_gap": 0.25,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.2,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "RF50_CPWG",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": -1,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.164,
"tuning_profile": "",
"via_diameter": 0.45,
"via_drill": 0.225,
"wire_width": 6
},
{
"bus_width": 12,
"clearance": 0.15,
"diff_pair_gap": 0.15,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.135,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "USB90_DIFF",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": -1,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.135,
"tuning_profile": "",
"via_diameter": 0.45,
"via_drill": 0.225,
"wire_width": 6
}
],
"meta": {
"version": 5
},
"net_colors": null,
"netclass_assignments": null,
"netclass_patterns": [
{
"netclass": "RF50_CPWG",
"pattern": "RF_SMA"
},
{
"netclass": "RF50_CPWG",
"pattern": "RF_PRE_SAW"
},
{
"netclass": "RF50_CPWG",
"pattern": "RF_POST_SAW"
},
{
"netclass": "RF50_CPWG",
"pattern": "MAX_MIXIN_HI"
},
{
"netclass": "USB90_DIFF",
"pattern": "USB_D_P"
},
{
"netclass": "USB90_DIFF",
"pattern": "USB_D_N"
},
{
"netclass": "USB90_DIFF",
"pattern": "FTDI_D_P"
},
{
"netclass": "USB90_DIFF",
"pattern": "FTDI_D_N"
},
{
"netclass": "CLOCK",
"pattern": "MAX_CLKOUT*"
},
{
"netclass": "CLOCK",
"pattern": "MAX_SCLK*"
},
{
"netclass": "CLOCK",
"pattern": "MAX_ADC_CLKIN"
},
{
"netclass": "CLOCK",
"pattern": "QSPI_SCLK"
},
{
"netclass": "CLOCK",
"pattern": "CLK_40M_FPGA"
},
{
"netclass": "CLOCK",
"pattern": "OSC40_OUT"
},
{
"netclass": "CLOCK",
"pattern": "TCXO16_OUT"
},
{
"netclass": "CLOCK",
"pattern": "FTDI_OSC12"
},
{
"netclass": "CLOCK",
"pattern": "FTDI_OSCI"
},
{
"netclass": "CLOCK",
"pattern": "IMU_SCK*"
},
{
"netclass": "CLOCK",
"pattern": "*TCK*"
},
{
"netclass": "POWER",
"pattern": "VCC*"
},
{
"netclass": "POWER",
"pattern": "SYS5"
},
{
"netclass": "POWER",
"pattern": "VBUS_RAW"
},
{
"netclass": "POWER",
"pattern": "USB5_PROT"
},
{
"netclass": "POWER",
"pattern": "EXT5_*"
},
{
"netclass": "POWER",
"pattern": "GND"
},
{
"netclass": "POWER",
"pattern": "CORE_SW"
},
{
"netclass": "POWER",
"pattern": "BUCK1V8_SW"
},
{
"netclass": "POWER",
"pattern": "BUCK3V3_SW"
},
{
"netclass": "POWER",
"pattern": "ANT_BIAS_SEL"
},
{
"netclass": "POWER",
"pattern": "ANT_BIAS_SW"
},
{
"netclass": "POWER",
"pattern": "FTDI_1V8"
},
{
"netclass": "POWER",
"pattern": "FTDI_VPHY"
},
{
"netclass": "POWER",
"pattern": "FTDI_VPLL"
},
{
"netclass": "POWER",
"pattern": "IMU_3V3"
},
{
"netclass": "POWER",
"pattern": "IMU_VREGA2"
},
{
"netclass": "POWER",
"pattern": "IMU_VREGD"
},
{
"netclass": "POWER",
"pattern": "USB_SHIELD"
},
{
"netclass": "BGA_ESCAPE",
"pattern": "EXP_*"
},
{
"netclass": "BGA_ESCAPE",
"pattern": "PMOD*"
}
]
},
"pcbnew": {
"last_paths": {
"idf": "",
"netlist": "",
"plot": "",
"specctra_dsn": "",
"step": "",
"vrml": ""
},
"page_layout_descr_file": ""
},
"schematic": {
"bus_aliases": {},
"legacy_lib_dir": "",
"legacy_lib_list": [],
"top_level_sheets": [
{
"filename": "board_routed.kicad_sch",
"name": "board_routed",
"uuid": "00000000-0000-0000-0000-000000000000"
}
]
},
"sheets": [],
"text_variables": {},
"tuning_profiles": {
"meta": {
"version": 0
},
"tuning_profiles_impedance_geometric": []
}
}
+510
View File
@@ -0,0 +1,510 @@
{
"tracks": [
{
"net": "RF_SMA",
"layer": "F.Cu",
"width": 0.164,
"points": [
[
174.5,
42.0
],
[
170.1,
42.09
],
[
169.5,
42.1
],
[
168.15,
42.1
]
]
},
{
"net": "RF_SMA",
"layer": "F.Cu",
"width": 0.164,
"points": [
[
170.1,
40.75
],
[
170.1,
42.09
]
]
},
{
"net": "RF_PRE_SAW",
"layer": "F.Cu",
"width": 0.164,
"points": [
[
166.85,
42.1
],
[
164.5,
42.1
]
]
},
{
"net": "RF_POST_SAW",
"layer": "F.Cu",
"width": 0.164,
"points": [
[
163.5,
42.3875
],
[
162.15,
41.93
],
[
161.01,
42.1
]
]
},
{
"net": "MAX_MIXIN_HI",
"layer": "F.Cu",
"width": 0.164,
"points": [
[
159.99,
42.1
],
[
159.69,
41.8
],
[
155.5,
41.8
],
[
155.5,
42.6
]
]
},
{
"net": "MAX_MIXIN_HI",
"layer": "F.Cu",
"width": 0.164,
"points": [
[
159.0,
41.18
],
[
159.0,
41.8
]
]
},
{
"net": "USB_D_N",
"layer": "F.Cu",
"width": 0.135,
"points": [
[
26.78,
47.25
],
[
28.6,
47.25
]
]
},
{
"net": "USB_D_P",
"layer": "F.Cu",
"width": 0.135,
"points": [
[
26.78,
47.75
],
[
29.2,
47.75
]
]
},
{
"net": "USB_D_N",
"layer": "F.Cu",
"width": 0.135,
"points": [
[
26.78,
48.25
],
[
27.6,
48.25
]
]
},
{
"net": "USB_D_P",
"layer": "F.Cu",
"width": 0.135,
"points": [
[
26.78,
48.75
],
[
28.2,
48.75
]
]
},
{
"net": "USB_D_N",
"layer": "L3_SIG",
"width": 0.135,
"points": [
[
27.6,
48.25
],
[
28.6,
47.25
]
]
},
{
"net": "USB_D_P",
"layer": "L3_SIG",
"width": 0.135,
"points": [
[
28.2,
48.75
],
[
29.2,
47.75
]
]
},
{
"net": "USB_D_N",
"layer": "F.Cu",
"width": 0.135,
"points": [
[
28.6,
47.25
],
[
29.6,
47.25
],
[
30.5,
48.15
],
[
30.5,
51.075
]
]
},
{
"net": "USB_D_P",
"layer": "F.Cu",
"width": 0.135,
"points": [
[
29.2,
47.75
],
[
29.45,
47.75
],
[
30.0,
48.3
],
[
30.0,
51.775
],
[
30.15,
51.925
]
]
},
{
"net": "USB_D_N",
"layer": "F.Cu",
"width": 0.135,
"points": [
[
30.5,
51.075
],
[
31.0,
51.075
],
[
34.425,
54.5
],
[
36.49,
54.5
]
]
},
{
"net": "USB_D_P",
"layer": "F.Cu",
"width": 0.135,
"points": [
[
30.15,
51.925
],
[
30.15,
52.5
],
[
31.718,
52.5
],
[
34.718,
55.5
],
[
36.49,
55.5
]
]
},
{
"net": "FTDI_D_N",
"layer": "F.Cu",
"width": 0.135,
"points": [
[
37.51,
54.5
],
[
37.76,
54.75
],
[
39.0,
54.75
]
]
},
{
"net": "FTDI_D_P",
"layer": "F.Cu",
"width": 0.135,
"points": [
[
37.51,
55.5
],
[
37.76,
55.25
],
[
39.0,
55.25
]
]
}
],
"vias": [
{
"net": "USB_D_N",
"at": [
28.6,
47.25
],
"diameter": 0.45,
"drill": 0.225
},
{
"net": "USB_D_P",
"at": [
29.2,
47.75
],
"diameter": 0.45,
"drill": 0.225
},
{
"net": "USB_D_N",
"at": [
27.6,
48.25
],
"diameter": 0.45,
"drill": 0.225
},
{
"net": "USB_D_P",
"at": [
28.2,
48.75
],
"diameter": 0.45,
"drill": 0.225
}
],
"rule_areas": [
{
"name": "USB_ESD_BREAKOUT",
"layers": [
"F.Cu"
],
"rect": [
29.9,
50.7,
32.1,
53.1
]
},
{
"name": "RF_NO_DIGITAL",
"layers": [
"B.Cu",
"In2.Cu",
"In4.Cu",
"In5.Cu"
],
"rect": [
152.0,
36.0,
174.5,
43.7
]
}
],
"ground_zones": [
{
"net": "GND",
"layer": "F.Cu",
"clearance": 0.2,
"min_thickness": 0.1,
"priority": 100,
"outline": [
[
152.0,
36.0
],
[
174.5,
36.0
],
[
174.5,
49.5
],
[
152.0,
49.5
]
],
"via_fence": [
[
153,
38.7
],
[
155,
38.7
],
[
157,
38.7
],
[
159,
38.7
],
[
161,
38.7
],
[
163,
38.7
],
[
165,
38.7
],
[
167,
38.7
],
[
169,
38.7
],
[
171,
38.7
],
[
173,
38.7
],
[
161,
45.0
],
[
163,
45.0
],
[
165,
45.0
],
[
167,
45.0
],
[
169,
45.0
],
[
171,
45.0
],
[
173,
45.0
]
]
}
]
}
File diff suppressed because it is too large Load Diff
+185 -13
View File
@@ -34,7 +34,7 @@ GND
Wire Wire Line Wire Wire Line
2500 2150 2650 2150 2500 2150 2650 2150
Text GLabel 2650 2150 0 30 BiDi ~ 0 Text GLabel 2650 2150 0 30 BiDi ~ 0
VBUS_RAW DBG_VBUS_RAW
Wire Wire Line Wire Wire Line
1100 2100 950 2100 1100 2100 950 2100
Text GLabel 950 2100 2 30 BiDi ~ 0 Text GLabel 950 2100 2 30 BiDi ~ 0
@@ -51,7 +51,7 @@ NoConn ~ 2500 1950
Wire Wire Line Wire Wire Line
1100 1900 950 1900 1100 1900 950 1900
Text GLabel 950 1900 2 30 BiDi ~ 0 Text GLabel 950 1900 2 30 BiDi ~ 0
VBUS_RAW DBG_VBUS_RAW
Wire Wire Line Wire Wire Line
2500 1850 2650 1850 2500 1850 2650 1850
Text GLabel 2650 1850 0 30 BiDi ~ 0 Text GLabel 2650 1850 0 30 BiDi ~ 0
@@ -63,7 +63,7 @@ GND
Wire Wire Line Wire Wire Line
2500 1750 2650 1750 2500 1750 2650 1750
Text GLabel 2650 1750 0 30 BiDi ~ 0 Text GLabel 2650 1750 0 30 BiDi ~ 0
VBUS_RAW DBG_VBUS_RAW
Wire Wire Line Wire Wire Line
1100 1700 950 1700 1100 1700 950 1700
Text GLabel 950 1700 2 30 BiDi ~ 0 Text GLabel 950 1700 2 30 BiDi ~ 0
@@ -80,7 +80,7 @@ NoConn ~ 2500 1550
Wire Wire Line Wire Wire Line
1100 1500 950 1500 1100 1500 950 1500
Text GLabel 950 1500 2 30 BiDi ~ 0 Text GLabel 950 1500 2 30 BiDi ~ 0
VBUS_RAW DBG_VBUS_RAW
Wire Wire Line Wire Wire Line
2500 1450 2650 1450 2500 1450 2650 1450
Text GLabel 2650 1450 0 30 BiDi ~ 0 Text GLabel 2650 1450 0 30 BiDi ~ 0
@@ -120,14 +120,8 @@ Text GLabel 2650 3450 0 30 BiDi ~ 0
USB_VBUS_DET USB_VBUS_DET
NoConn ~ 1100 3350 NoConn ~ 1100 3350
NoConn ~ 2500 3350 NoConn ~ 2500 3350
Wire Wire Line NoConn ~ 1100 3250
1100 3250 950 3250 NoConn ~ 2500 3250
Text GLabel 950 3250 2 30 BiDi ~ 0
TUSB_OUT1
Wire Wire Line
2500 3250 2650 3250
Text GLabel 2650 3250 0 30 BiDi ~ 0
TUSB_OUT2
NoConn ~ 1100 3150 NoConn ~ 1100 3150
Wire Wire Line Wire Wire Line
2500 3150 2650 3150 2500 3150 2650 3150
@@ -157,7 +151,7 @@ $EndComp
Wire Wire Line Wire Wire Line
1100 4500 950 4500 1100 4500 950 4500
Text GLabel 950 4500 2 30 BiDi ~ 0 Text GLabel 950 4500 2 30 BiDi ~ 0
VBUS_RAW DBG_VBUS_RAW
Wire Wire Line Wire Wire Line
2500 4500 2650 4500 2500 4500 2650 4500
Text GLabel 2650 4500 0 30 BiDi ~ 0 Text GLabel 2650 4500 0 30 BiDi ~ 0
@@ -4555,4 +4549,182 @@ Wire Wire Line
17500 12200 17650 12200 17500 12200 17650 12200
Text GLabel 17650 12200 0 30 BiDi ~ 0 Text GLabel 17650 12200 0 30 BiDi ~ 0
GND GND
$Comp
L BOARD_J7 J7
U 1 1 650100C4
P 16800 13000
F 0 "J7" H 16800 12900 50 0000 C CNN
F 1 "USB-C power input" H 16800 13100 50 0000 C CNN
F 2 "Connector_USB:USB_C_Receptacle_GCT_USB4105-xx-A_16P_TopMnt_Horizontal" H 16800 12800 50 0001 C CNN
F 3 "" H 16800 13000 50 0001 C CNN
F 4 "USB4105-GF-A-120" H 16800 13000 50 0001 C CNN "MPN"
F 5 "GCT" H 16800 13000 50 0001 C CNN "Manufacturer"
1 16800 13000
1 0 0 -1
$EndComp
Wire Wire Line
16100 13400 15950 13400
Text GLabel 15950 13400 2 30 BiDi ~ 0
GND
Wire Wire Line
17500 13350 17650 13350
Text GLabel 17650 13350 0 30 BiDi ~ 0
VBUS_RAW
Wire Wire Line
16100 13300 15950 13300
Text GLabel 15950 13300 2 30 BiDi ~ 0
PWR_CC1
NoConn ~ 17500 13250
NoConn ~ 16100 13200
NoConn ~ 17500 13150
Wire Wire Line
16100 13100 15950 13100
Text GLabel 15950 13100 2 30 BiDi ~ 0
VBUS_RAW
Wire Wire Line
17500 13050 17650 13050
Text GLabel 17650 13050 0 30 BiDi ~ 0
GND
Wire Wire Line
16100 13000 15950 13000
Text GLabel 15950 13000 2 30 BiDi ~ 0
GND
Wire Wire Line
17500 12950 17650 12950
Text GLabel 17650 12950 0 30 BiDi ~ 0
VBUS_RAW
Wire Wire Line
16100 12900 15950 12900
Text GLabel 15950 12900 2 30 BiDi ~ 0
PWR_CC2
NoConn ~ 17500 12850
NoConn ~ 16100 12800
NoConn ~ 17500 12750
Wire Wire Line
16100 12700 15950 12700
Text GLabel 15950 12700 2 30 BiDi ~ 0
VBUS_RAW
Wire Wire Line
17500 12650 17650 12650
Text GLabel 17650 12650 0 30 BiDi ~ 0
GND
Wire Wire Line
16100 12600 15950 12600
Text GLabel 15950 12600 2 30 BiDi ~ 0
USB_SHIELD
$Comp
L BOARD_U35 U35
U 1 1 650100C5
P 16800 14500
F 0 "U35" H 16800 14400 50 0000 C CNN
F 1 "TUSB320LAI" H 16800 14600 50 0000 C CNN
F 2 "Package_DFN_QFN:Texas_X2QFN-12_1.6x1.6mm_P0.4mm" H 16800 14300 50 0001 C CNN
F 3 "" H 16800 14500 50 0001 C CNN
F 4 "TUSB320LAIRSW(R)" H 16800 14500 50 0001 C CNN "MPN"
F 5 "Texas Instruments" H 16800 14500 50 0001 C CNN "Manufacturer"
1 16800 14500
1 0 0 -1
$EndComp
Wire Wire Line
16100 14750 15950 14750
Text GLabel 15950 14750 2 30 BiDi ~ 0
PWR_CC1
Wire Wire Line
17500 14750 17650 14750
Text GLabel 17650 14750 0 30 BiDi ~ 0
PWR_CC2
Wire Wire Line
16100 14650 15950 14650
Text GLabel 15950 14650 2 30 BiDi ~ 0
GND
Wire Wire Line
17500 14650 17650 14650
Text GLabel 17650 14650 0 30 BiDi ~ 0
PWR_USB_VBUS_DET
NoConn ~ 16100 14550
NoConn ~ 17500 14550
Wire Wire Line
16100 14450 15950 14450
Text GLabel 15950 14450 2 30 BiDi ~ 0
TUSB_OUT1
Wire Wire Line
17500 14450 17650 14450
Text GLabel 17650 14450 0 30 BiDi ~ 0
TUSB_OUT2
NoConn ~ 16100 14350
Wire Wire Line
17500 14350 17650 14350
Text GLabel 17650 14350 0 30 BiDi ~ 0
GND
Wire Wire Line
16100 14250 15950 14250
Text GLabel 15950 14250 2 30 BiDi ~ 0
GND
Wire Wire Line
17500 14250 17650 14250
Text GLabel 17650 14250 0 30 BiDi ~ 0
VCC3V3_AUX
$Comp
L BOARD_R107 R107
U 1 1 650100C6
P 16800 15700
F 0 "R107" H 16800 15600 50 0000 C CNN
F 1 "900K" H 16800 15800 50 0000 C CNN
F 2 "Resistor_SMD:R_0402_1005Metric" H 16800 15500 50 0001 C CNN
F 3 "" H 16800 15700 50 0001 C CNN
F 4 "RC0402FR-07909KL" H 16800 15700 50 0001 C CNN "MPN"
F 5 "Yageo" H 16800 15700 50 0001 C CNN "Manufacturer"
1 16800 15700
1 0 0 -1
$EndComp
Wire Wire Line
16100 15700 15950 15700
Text GLabel 15950 15700 2 30 BiDi ~ 0
VBUS_RAW
Wire Wire Line
17500 15700 17650 15700
Text GLabel 17650 15700 0 30 BiDi ~ 0
PWR_USB_VBUS_DET
$Comp
L BOARD_C129 C129
U 1 1 650100C7
P 16800 16500
F 0 "C129" H 16800 16400 50 0000 C CNN
F 1 "100nF" H 16800 16600 50 0000 C CNN
F 2 "Capacitor_SMD:C_0402_1005Metric" H 16800 16300 50 0001 C CNN
F 3 "" H 16800 16500 50 0001 C CNN
F 4 "GRM155R71A104KA01D" H 16800 16500 50 0001 C CNN "MPN"
F 5 "Murata" H 16800 16500 50 0001 C CNN "Manufacturer"
1 16800 16500
1 0 0 -1
$EndComp
Wire Wire Line
16100 16500 15950 16500
Text GLabel 15950 16500 2 30 BiDi ~ 0
VCC3V3_AUX
Wire Wire Line
17500 16500 17650 16500
Text GLabel 17650 16500 0 30 BiDi ~ 0
GND
$Comp
L BOARD_R108 R108
U 1 1 650100C8
P 16800 17300
F 0 "R108" H 16800 17200 50 0000 C CNN
F 1 "10K" H 16800 17400 50 0000 C CNN
F 2 "Resistor_SMD:R_0402_1005Metric" H 16800 17100 50 0001 C CNN
F 3 "" H 16800 17300 50 0001 C CNN
F 4 "RC0402FR-0710KL" H 16800 17300 50 0001 C CNN "MPN"
F 5 "Yageo" H 16800 17300 50 0001 C CNN "Manufacturer"
1 16800 17300
1 0 0 -1
$EndComp
Wire Wire Line
16100 17300 15950 17300
Text GLabel 15950 17300 2 30 BiDi ~ 0
VCC3V3_AUX
Wire Wire Line
17500 17300 17650 17300
Text GLabel 17650 17300 0 30 BiDi ~ 0
MUX_ST
$EndSCHEMATC $EndSCHEMATC
+52
View File
@@ -0,0 +1,52 @@
# Fixed-route data files
`power_routes.json` and `critical_routes.json` hold the **hand-drawn, audited
geometry** that used to live in `scripts/route_power_stage.py` and the RF/USB
block of `scripts/finalize_routing.py`. They are board-specific layout data —
not an algorithm — consumed by the project-independent loader
```
pcbrouter fixed --in <in.kicad_pcb> --out <out.kicad_pcb> --routes <file.json>
```
which resolves the anchors against the board's footprints and splices locked
copper, rule areas and the RF ground zone into the `.kicad_pcb`. Run
`kicad-cli pcb drc --refill-zones` afterwards so the planes re-carve around the
new vias (the Python scripts called `ZONE_FILLER.Fill()` for the same reason).
Regenerate from the (retired) scripts with
`/usr/bin/python3 scripts/_extract_fixed_routes.py`.
## Schema
```jsonc
{
"tracks": [
{
"net": "CORE_SW", // net name (must exist on the board)
"layer": "F.Cu", // KiCad canonical or user layer name
"width": 0.20, // mm
"points": [ "U13.9", [99.25, 95.25], [100.0, 95.25] ]
// each point: "REF.PAD" anchor OR [x, y] mm
}
],
"vias": [
{ "net": "VCC0V95", "at": [107.8, 92.6], "diameter": 0.75, "drill": 0.38 }
// "at" may also be a "REF.PAD" anchor
],
"rule_areas": [
{ "name": "RF_NO_DIGITAL",
"layers": ["B.Cu", "In2.Cu", "In4.Cu", "In5.Cu"],
"rect": [152.0, 36.0, 174.5, 49.5] } // x0 y0 x1 y1 mm
],
"ground_zones": [
{ "net": "GND", "layer": "F.Cu",
"clearance": 0.20, "min_thickness": 0.10, "priority": 100,
"outline": [[152.0,36.0],[174.5,36.0],[174.5,49.5],[152.0,49.5]],
"via_fence": [[153,38.7],[155,38.7], ...] } // 0.45/0.225 GND vias
]
}
```
All coordinates are millimetres in the board's own frame (KiCad screen-down Y).
Anchors are resolved to the pad centre. Every emitted item is `(locked yes)`.
+1 -1
View File
@@ -271,7 +271,7 @@ $EndComp
Wire Wire Line Wire Wire Line
1100 7700 950 7700 1100 7700 950 7700
Text GLabel 950 7700 2 30 BiDi ~ 0 Text GLabel 950 7700 2 30 BiDi ~ 0
VBUS_RAW DBG_VBUS_RAW
Wire Wire Line Wire Wire Line
2500 7700 2650 7700 2500 7700 2650 7700
Text GLabel 2650 7700 0 30 BiDi ~ 0 Text GLabel 2650 7700 0 30 BiDi ~ 0
+19 -6
View File
@@ -7,12 +7,25 @@ anlamlı teslim çıktıları tutulur:
- `jlcpcb_bom.csv`: montaj servisi için sadeleştirilmiş BOM. - `jlcpcb_bom.csv`: montaj servisi için sadeleştirilmiş BOM.
- `erc_final_reset.rpt`: güncel şema ERC raporu; 0 ihlal. - `erc_final_reset.rpt`: güncel şema ERC raporu; 0 ihlal.
- `drc_final_placement.rpt`: unrouted placement DRC raporu. - `drc_final_placement.rpt`: unrouted placement DRC raporu.
- `drc_power_local.rpt`: sinyal routing'inden önceki güç katı DRC raporu.
- `drc_final_routed.rpt`: routed kart DRC raporu.
- `board_final_reset_top.png`: güncel üst görünüm. - `board_final_reset_top.png`: güncel üst görünüm.
Eski `.dsn`, `.ses`, preview PCB ve ara DRC raporları yeni netlist için Routing çalışma dosyaları:
authoritative değildir. Routing her netlist/placement değişiminden sonra güncel
`hardware/board.kicad_pcb` üzerinden yeniden başlatılmalıdır.
Güncel placement raporundaki 12 `isolated_copper` ve 499 unconnected kayıt, - `signal_stage.dsn` / `signal_stage.ses`: autorouter'a verilen ve ondan alınan
kartın henüz route edilmemiş olduğunu gösterir. Bunlar üretime hazır olunduğu sinyal katı. Sadece `board_power_local.kicad_pcb` ile birlikte anlamlıdır.
anlamına gelmez; final routed raporda sıfır açık bağlantı hedeflenir. - `signal_stage_preview.kicad_pcb`: DSN'e giren bakırın KiCad'de incelenebilir
hâli.
- `freerouting_signal.log`: pass başına unrouted/violation sayısı.
Eski `.dsn`, `.ses`, preview PCB ve ara DRC raporları yeni netlist için
authoritative değildir. `.dsn` her zaman onu üreten karta bağlıdır: placement,
güç bakırı veya keepout değiştiğinde export ve routing baştan yapılmalıdır,
yoksa import edilen sonuç mevcut bakırla çakışır.
Final routed raporda sıfır açık bağlantı, sıfır clearance/short ve sıfır
schematic-parity hatası hedeflenir. Unrouted aşamadaki `unconnected_items`,
`via_dangling` ve `isolated_copper` kayıtları normaldir: sırasıyla henüz
çekilmemiş bağlantıları, henüz kullanılmayan BGA escape via'larını ve final
fill'de silinecek plane parçalarını gösterir.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 KiB

After

Width:  |  Height:  |  Size: 332 KiB

+8 -5
View File
@@ -1,5 +1,5 @@
Reference,Qty,Value,Manufacturer,MPN,Footprint,Assembly,DNP,Description Reference,Qty,Value,Manufacturer,MPN,Footprint,Assembly,DNP,Description
C1,1,100nF,Murata,GRM155R71A104KA01D,Capacitor_SMD:C_0402_1005Metric,SMT,no,TUSB320 local bypass "C1,C129",2,100nF,Murata,GRM155R71A104KA01D,Capacitor_SMD:C_0402_1005Metric,SMT,no,TUSB320 local bypass
"C10,C11",2,47uF,Murata,GRM32ER70J476ME19L,Capacitor_SMD:C_1210_3225Metric,SMT,no,5-V system bulk "C10,C11",2,47uF,Murata,GRM32ER70J476ME19L,Capacitor_SMD:C_1210_3225Metric,SMT,no,5-V system bulk
C100,1,100nF,Murata,GRM155R71A104KA01D,Capacitor_SMD:C_0402_1005Metric,SMT,no,16.368-MHz TCXO bypass C100,1,100nF,Murata,GRM155R71A104KA01D,Capacitor_SMD:C_0402_1005Metric,SMT,no,16.368-MHz TCXO bypass
C101,1,1uF,Murata,GRM155R60J105KE19D,Capacitor_SMD:C_0402_1005Metric,SMT,no,Clock-rail local bulk C101,1,1uF,Murata,GRM155R60J105KE19D,Capacitor_SMD:C_0402_1005Metric,SMT,no,Clock-rail local bulk
@@ -80,11 +80,12 @@ FB1,1,600R@100MHz,Murata,BLM15AG601SN1D,Inductor_SMD:L_0402_1005Metric,SMT,no,IM
FB2,1,600R@100MHz,Murata,BLM15AG601SN1D,Inductor_SMD:L_0402_1005Metric,SMT,no,FTDI USB-PHY supply ferrite FB2,1,600R@100MHz,Murata,BLM15AG601SN1D,Inductor_SMD:L_0402_1005Metric,SMT,no,FTDI USB-PHY supply ferrite
FB3,1,600R@100MHz,Murata,BLM15AG601SN1D,Inductor_SMD:L_0402_1005Metric,SMT,no,FTDI PLL supply ferrite FB3,1,600R@100MHz,Murata,BLM15AG601SN1D,Inductor_SMD:L_0402_1005Metric,SMT,no,FTDI PLL supply ferrite
FL1,1,GPS L1/L1C SAW,Kyocera,SF14-1582M5UUS2,board:Kyocera_SF14_5pad,SMT,no,"1565.19-1585.65-MHz SAW, 1.2-dB typical insertion loss" FL1,1,GPS L1/L1C SAW,Kyocera,SF14-1582M5UUS2,board:Kyocera_SF14_5pad,SMT,no,"1565.19-1585.65-MHz SAW, 1.2-dB typical insertion loss"
J1,1,USB-C USB2 sink,GCT,USB4105-GF-A-120,Connector_USB:USB_C_Receptacle_GCT_USB4105-xx-A_16P_TopMnt_Horizontal,SMT,no,"USB 2.0 Type-C receptacle, UFP/sink" J1,1,USB-C USB2 sink,GCT,USB4105-GF-A-120,Connector_USB:USB_C_Receptacle_GCT_USB4105-xx-A_16P_TopMnt_Horizontal,SMT,no,"USB 2.0 Type-C receptacle, UFP/sink, debug data only"
J2,1,GPS L1 SMA,Amphenol RF,132289,Connector_Coaxial:SMA_Amphenol_132289_EdgeMount,THT,no,Edge-launch 50-ohm active GNSS antenna input J2,1,GPS L1 SMA,Amphenol RF,132289,Connector_Coaxial:SMA_Amphenol_132289_EdgeMount,THT,no,Edge-launch 50-ohm active GNSS antenna input
J3,1,EXT_5V,Phoenix Contact,1935161,"TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-2_1x02_P5.00mm_Horizontal",THT,no,Clearly labelled external regulated 5-V input J3,1,EXT_5V,Phoenix Contact,1935161,"TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-2_1x02_P5.00mm_Horizontal",THT,no,Clearly labelled external regulated 5-V input
J4,1,EXT RESET_N,JST,SM02B-SRSS-TB(LF)(SN),Connector_JST:JST_SH_SM02B-SRSS-TB_1x02-1MP_P1.00mm_Horizontal,SMT,no,External active-low open-drain reset input; JST-SH 1.0-mm J4,1,EXT RESET_N,JST,SM02B-SRSS-TB(LF)(SN),Connector_JST:JST_SH_SM02B-SRSS-TB_1x02-1MP_P1.00mm_Horizontal,SMT,no,External active-low open-drain reset input; JST-SH 1.0-mm
J5,1,ANT BIAS OFF/3V3/5V,Wuerth Elektronik,61300311121,Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical,THT,no,One-shunt hardware antenna bias selection; no shunt is OFF J5,1,ANT BIAS OFF/3V3/5V,Wuerth Elektronik,61300311121,Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical,THT,no,One-shunt hardware antenna bias selection; no shunt is OFF
J7,1,USB-C power input,GCT,USB4105-GF-A-120,Connector_USB:USB_C_Receptacle_GCT_USB4105-xx-A_16P_TopMnt_Horizontal,SMT,no,"USB 2.0 Type-C receptacle, UFP/sink, power input only"
"JPMOD1,JPMOD2",2,PMOD 3.3 V,Amphenol ICC,67997-412HLF,board:Digilent_PMOD_2x6_P2.54mm,THT,no,"Digilent 2x6 PMOD: eight 3.3 V GPIO, two GND, two 3.3 V" "JPMOD1,JPMOD2",2,PMOD 3.3 V,Amphenol ICC,67997-412HLF,board:Digilent_PMOD_2x6_P2.54mm,THT,no,"Digilent 2x6 PMOD: eight 3.3 V GPIO, two GND, two 3.3 V"
"JPMOD3,JPMOD4",2,PMOD 1.8 V,Amphenol ICC,67997-412HLF,board:Digilent_PMOD_2x6_P2.54mm,THT,no,"Digilent 2x6 PMOD: eight 1.8 V GPIO, two GND, two 1.8 V" "JPMOD3,JPMOD4",2,PMOD 1.8 V,Amphenol ICC,67997-412HLF,board:Digilent_PMOD_2x6_P2.54mm,THT,no,"Digilent 2x6 PMOD: eight 1.8 V GPIO, two GND, two 1.8 V"
L1,1,100nH,Coilcraft,XEL4030-101MEC,board:Coilcraft_XEL4030,SMT,no,"Core inductor: 30-A saturation, 1.5-mohm DCR" L1,1,100nH,Coilcraft,XEL4030-101MEC,board:Coilcraft_XEL4030,SMT,no,"Core inductor: 30-A saturation, 1.5-mohm DCR"
@@ -100,6 +101,7 @@ R103,1,1K,Yageo,RC0402FR-071KL,Resistor_SMD:R_0402_1005Metric,SMT,no,External-re
R104,1,10K,Yageo,RC0402FR-0710KL,Resistor_SMD:R_0402_1005Metric,SMT,no,LM3880 final-flag pull-up for all-power logic R104,1,10K,Yageo,RC0402FR-0710KL,Resistor_SMD:R_0402_1005Metric,SMT,no,LM3880 final-flag pull-up for all-power logic
R105,1,10K,Yageo,RC0402FR-0710KL,Resistor_SMD:R_0402_1005Metric,SMT,no,1.8-V power-good pull-up for all-power logic R105,1,10K,Yageo,RC0402FR-0710KL,Resistor_SMD:R_0402_1005Metric,SMT,no,1.8-V power-good pull-up for all-power logic
R106,1,10K,Yageo,RC0402FR-0710KL,Resistor_SMD:R_0402_1005Metric,SMT,no,3.3-V power-good pull-up for all-power logic R106,1,10K,Yageo,RC0402FR-0710KL,Resistor_SMD:R_0402_1005Metric,SMT,no,3.3-V power-good pull-up for all-power logic
R108,1,10K,Yageo,RC0402FR-0710KL,Resistor_SMD:R_0402_1005Metric,SMT,no,"TPS2121 open-drain status pull-up (RST, 6-20 kohm)"
"R11,R16",2,232K,Yageo,RC0402FR-07232KL,Resistor_SMD:R_0402_1005Metric,SMT,no,4.0-V nominal eFuse UV divider top "R11,R16",2,232K,Yageo,RC0402FR-07232KL,Resistor_SMD:R_0402_1005Metric,SMT,no,4.0-V nominal eFuse UV divider top
"R12,R17",2,100K,Yageo,RC0402FR-07100KL,Resistor_SMD:R_0402_1005Metric,SMT,no,eFuse UV divider bottom "R12,R17",2,100K,Yageo,RC0402FR-07100KL,Resistor_SMD:R_0402_1005Metric,SMT,no,eFuse UV divider bottom
"R13,R18",2,383K,Yageo,RC0402FR-07383KL,Resistor_SMD:R_0402_1005Metric,SMT,no,5.8-V nominal eFuse OV divider top "R13,R18",2,383K,Yageo,RC0402FR-07383KL,Resistor_SMD:R_0402_1005Metric,SMT,no,5.8-V nominal eFuse OV divider top
@@ -134,7 +136,7 @@ R44,1,33R,Yageo,RC0402FR-0733RL,Resistor_SMD:R_0402_1005Metric,SMT,no,40-MHz ADC
R55,1,100K,Yageo,RC0402FR-07100KL,Resistor_SMD:R_0402_1005Metric,SMT,no,MAX2771 remains shut down before FPGA configuration R55,1,100K,Yageo,RC0402FR-07100KL,Resistor_SMD:R_0402_1005Metric,SMT,no,MAX2771 remains shut down before FPGA configuration
R56,1,33R,Yageo,RC0402FR-0733RL,Resistor_SMD:R_0402_1005Metric,SMT,no,40-MHz FPGA-clock source termination R56,1,33R,Yageo,RC0402FR-0733RL,Resistor_SMD:R_0402_1005Metric,SMT,no,40-MHz FPGA-clock source termination
"R57,R58,R59",3,22R,Yageo,RC0402FR-0722RL,Resistor_SMD:R_0402_1005Metric,SMT,no,SafeSPI source-series damping "R57,R58,R59",3,22R,Yageo,RC0402FR-0722RL,Resistor_SMD:R_0402_1005Metric,SMT,no,SafeSPI source-series damping
R6,1,900K,Yageo,RC0402FR-07909KL,Resistor_SMD:R_0402_1005Metric,SMT,no,TUSB320 VBUS_DET current limiter "R6,R107",2,900K,Yageo,RC0402FR-07909KL,Resistor_SMD:R_0402_1005Metric,SMT,no,TUSB320 VBUS_DET current limiter
R60,1,22R,Yageo,RC0402FR-0722RL,Resistor_SMD:R_0402_1005Metric,SMT,no,SafeSPI MISO source damping at IMU R60,1,22R,Yageo,RC0402FR-0722RL,Resistor_SMD:R_0402_1005Metric,SMT,no,SafeSPI MISO source damping at IMU
R61,1,10K,Yageo,RC0402FR-0710KL,Resistor_SMD:R_0402_1005Metric,SMT,no,Hold IMU reset until FPGA releases it R61,1,10K,Yageo,RC0402FR-0710KL,Resistor_SMD:R_0402_1005Metric,SMT,no,Hold IMU reset until FPGA releases it
R62,1,27R,Yageo,RC0402FR-0727RL,Resistor_SMD:R_0402_1005Metric,SMT,no,FTDI USB D+ series termination R62,1,27R,Yageo,RC0402FR-0727RL,Resistor_SMD:R_0402_1005Metric,SMT,no,FTDI USB D+ series termination
@@ -204,7 +206,8 @@ U31,1,SN74LVC1G07,Texas Instruments,SN74LVC1G07DBVR,Package_TO_SOT_SMD:SOT-23-5,
U32,1,SN74LVC1G07,Texas Instruments,SN74LVC1G07DBVR,Package_TO_SOT_SMD:SOT-23-5,SMT,no,Open-drain all-rail-valid translation into FPGA CRESET_N U32,1,SN74LVC1G07,Texas Instruments,SN74LVC1G07DBVR,Package_TO_SOT_SMD:SOT-23-5,SMT,no,Open-drain all-rail-valid translation into FPGA CRESET_N
U33,1,SN74LVC1G07,Texas Instruments,SN74LVC1G07DBVR,Package_TO_SOT_SMD:SOT-23-5,SMT,no,Open-drain external/manual reset isolation into FPGA CRESET_N U33,1,SN74LVC1G07,Texas Instruments,SN74LVC1G07DBVR,Package_TO_SOT_SMD:SOT-23-5,SMT,no,Open-drain external/manual reset isolation into FPGA CRESET_N
U34,1,SN74LVC08A,Texas Instruments,SN74LVC08APWR,Package_SO:TSSOP-14_4.4x5mm_P0.65mm,SMT,no,Always-on all-rail power-valid logic for reset release and debug isolation U34,1,SN74LVC08A,Texas Instruments,SN74LVC08APWR,Package_SO:TSSOP-14_4.4x5mm_P0.65mm,SMT,no,Always-on all-rail power-valid logic for reset release and debug isolation
U5,1,TUSB320LAI,Texas Instruments,TUSB320LAIRSW(R),Package_DFN_QFN:Texas_X2QFN-12_1.6x1.6mm_P0.4mm,SMT,no,USB Type-C port controller in UFP GPIO mode; integrated Rd on CC1/CC2 U35,1,TUSB320LAI,Texas Instruments,TUSB320LAIRSW(R),Package_DFN_QFN:Texas_X2QFN-12_1.6x1.6mm_P0.4mm,SMT,no,USB Type-C port controller in UFP GPIO mode for the power input; integrated Rd on CC1/CC2; OUT1/OUT2 3-A advertisement drives U10/U11 MAIN_EN gating
U5,1,TUSB320LAI,Texas Instruments,TUSB320LAIRSW(R),Package_DFN_QFN:Texas_X2QFN-12_1.6x1.6mm_P0.4mm,SMT,no,USB Type-C port controller in UFP GPIO mode; integrated Rd on CC1/CC2; current-advertisement outputs unused since this port never sources SYS5
U6,1,TPD2EUSB30A,Texas Instruments,TPD2EUSB30ADRTR,Package_TO_SOT_SMD:Texas_DRT-3,SMT,no,Low-capacitance USB 2.0 ESD protection U6,1,TPD2EUSB30A,Texas Instruments,TPD2EUSB30ADRTR,Package_TO_SOT_SMD:Texas_DRT-3,SMT,no,Low-capacitance USB 2.0 ESD protection
"U7,U8",2,TPS259470A,Texas Instruments,TPS259470ARPWR,board:Texas_RPW0010A_QFN10,SMT,no,True-reverse-blocking eFuse; UV/OV/current/inrush protection "U7,U8",2,TPS259470A,Texas Instruments,TPS259470ARPWR,board:Texas_RPW0010A_QFN10,SMT,no,True-reverse-blocking eFuse; UV/OV/current/inrush protection
U9,1,TPS2121,Texas Instruments,TPS2121RUXR,board:Texas_RUX0012A_VQFN12,SMT,no,4.5-A power mux; external input wins near-equal valid 5-V inputs U9,1,TPS2121,Texas Instruments,TPS2121RUXR,board:Texas_RUX0012A_VQFN12,SMT,no,4.5-A power mux. PR1=GND disables priority mode; CP2 tied to VCC3V3_AUX is what selects IN2 (external 5 V) once the aux rail is up
1 Reference Qty Value Manufacturer MPN Footprint Assembly DNP Description
2 C1 C1,C129 1 2 100nF Murata GRM155R71A104KA01D Capacitor_SMD:C_0402_1005Metric SMT no TUSB320 local bypass
3 C10,C11 2 47uF Murata GRM32ER70J476ME19L Capacitor_SMD:C_1210_3225Metric SMT no 5-V system bulk
4 C100 1 100nF Murata GRM155R71A104KA01D Capacitor_SMD:C_0402_1005Metric SMT no 16.368-MHz TCXO bypass
5 C101 1 1uF Murata GRM155R60J105KE19D Capacitor_SMD:C_0402_1005Metric SMT no Clock-rail local bulk
80 FB2 1 600R@100MHz Murata BLM15AG601SN1D Inductor_SMD:L_0402_1005Metric SMT no FTDI USB-PHY supply ferrite
81 FB3 1 600R@100MHz Murata BLM15AG601SN1D Inductor_SMD:L_0402_1005Metric SMT no FTDI PLL supply ferrite
82 FL1 1 GPS L1/L1C SAW Kyocera SF14-1582M5UUS2 board:Kyocera_SF14_5pad SMT no 1565.19-1585.65-MHz SAW, 1.2-dB typical insertion loss
83 J1 1 USB-C USB2 sink GCT USB4105-GF-A-120 Connector_USB:USB_C_Receptacle_GCT_USB4105-xx-A_16P_TopMnt_Horizontal SMT no USB 2.0 Type-C receptacle, UFP/sink USB 2.0 Type-C receptacle, UFP/sink, debug data only
84 J2 1 GPS L1 SMA Amphenol RF 132289 Connector_Coaxial:SMA_Amphenol_132289_EdgeMount THT no Edge-launch 50-ohm active GNSS antenna input
85 J3 1 EXT_5V Phoenix Contact 1935161 TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-2_1x02_P5.00mm_Horizontal THT no Clearly labelled external regulated 5-V input
86 J4 1 EXT RESET_N JST SM02B-SRSS-TB(LF)(SN) Connector_JST:JST_SH_SM02B-SRSS-TB_1x02-1MP_P1.00mm_Horizontal SMT no External active-low open-drain reset input; JST-SH 1.0-mm
87 J5 1 ANT BIAS OFF/3V3/5V Wuerth Elektronik 61300311121 Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical THT no One-shunt hardware antenna bias selection; no shunt is OFF
88 J7 1 USB-C power input GCT USB4105-GF-A-120 Connector_USB:USB_C_Receptacle_GCT_USB4105-xx-A_16P_TopMnt_Horizontal SMT no USB 2.0 Type-C receptacle, UFP/sink, power input only
89 JPMOD1,JPMOD2 2 PMOD 3.3 V Amphenol ICC 67997-412HLF board:Digilent_PMOD_2x6_P2.54mm THT no Digilent 2x6 PMOD: eight 3.3 V GPIO, two GND, two 3.3 V
90 JPMOD3,JPMOD4 2 PMOD 1.8 V Amphenol ICC 67997-412HLF board:Digilent_PMOD_2x6_P2.54mm THT no Digilent 2x6 PMOD: eight 1.8 V GPIO, two GND, two 1.8 V
91 L1 1 100nH Coilcraft XEL4030-101MEC board:Coilcraft_XEL4030 SMT no Core inductor: 30-A saturation, 1.5-mohm DCR
101 R104 1 10K Yageo RC0402FR-0710KL Resistor_SMD:R_0402_1005Metric SMT no LM3880 final-flag pull-up for all-power logic
102 R105 1 10K Yageo RC0402FR-0710KL Resistor_SMD:R_0402_1005Metric SMT no 1.8-V power-good pull-up for all-power logic
103 R106 1 10K Yageo RC0402FR-0710KL Resistor_SMD:R_0402_1005Metric SMT no 3.3-V power-good pull-up for all-power logic
104 R108 1 10K Yageo RC0402FR-0710KL Resistor_SMD:R_0402_1005Metric SMT no TPS2121 open-drain status pull-up (RST, 6-20 kohm)
105 R11,R16 2 232K Yageo RC0402FR-07232KL Resistor_SMD:R_0402_1005Metric SMT no 4.0-V nominal eFuse UV divider top
106 R12,R17 2 100K Yageo RC0402FR-07100KL Resistor_SMD:R_0402_1005Metric SMT no eFuse UV divider bottom
107 R13,R18 2 383K Yageo RC0402FR-07383KL Resistor_SMD:R_0402_1005Metric SMT no 5.8-V nominal eFuse OV divider top
136 R55 1 100K Yageo RC0402FR-07100KL Resistor_SMD:R_0402_1005Metric SMT no MAX2771 remains shut down before FPGA configuration
137 R56 1 33R Yageo RC0402FR-0733RL Resistor_SMD:R_0402_1005Metric SMT no 40-MHz FPGA-clock source termination
138 R57,R58,R59 3 22R Yageo RC0402FR-0722RL Resistor_SMD:R_0402_1005Metric SMT no SafeSPI source-series damping
139 R6 R6,R107 1 2 900K Yageo RC0402FR-07909KL Resistor_SMD:R_0402_1005Metric SMT no TUSB320 VBUS_DET current limiter
140 R60 1 22R Yageo RC0402FR-0722RL Resistor_SMD:R_0402_1005Metric SMT no SafeSPI MISO source damping at IMU
141 R61 1 10K Yageo RC0402FR-0710KL Resistor_SMD:R_0402_1005Metric SMT no Hold IMU reset until FPGA releases it
142 R62 1 27R Yageo RC0402FR-0727RL Resistor_SMD:R_0402_1005Metric SMT no FTDI USB D+ series termination
206 U32 1 SN74LVC1G07 Texas Instruments SN74LVC1G07DBVR Package_TO_SOT_SMD:SOT-23-5 SMT no Open-drain all-rail-valid translation into FPGA CRESET_N
207 U33 1 SN74LVC1G07 Texas Instruments SN74LVC1G07DBVR Package_TO_SOT_SMD:SOT-23-5 SMT no Open-drain external/manual reset isolation into FPGA CRESET_N
208 U34 1 SN74LVC08A Texas Instruments SN74LVC08APWR Package_SO:TSSOP-14_4.4x5mm_P0.65mm SMT no Always-on all-rail power-valid logic for reset release and debug isolation
209 U5 U35 1 TUSB320LAI Texas Instruments TUSB320LAIRSW(R) Package_DFN_QFN:Texas_X2QFN-12_1.6x1.6mm_P0.4mm SMT no USB Type-C port controller in UFP GPIO mode; integrated Rd on CC1/CC2 USB Type-C port controller in UFP GPIO mode for the power input; integrated Rd on CC1/CC2; OUT1/OUT2 3-A advertisement drives U10/U11 MAIN_EN gating
210 U5 1 TUSB320LAI Texas Instruments TUSB320LAIRSW(R) Package_DFN_QFN:Texas_X2QFN-12_1.6x1.6mm_P0.4mm SMT no USB Type-C port controller in UFP GPIO mode; integrated Rd on CC1/CC2; current-advertisement outputs unused since this port never sources SYS5
211 U6 1 TPD2EUSB30A Texas Instruments TPD2EUSB30ADRTR Package_TO_SOT_SMD:Texas_DRT-3 SMT no Low-capacitance USB 2.0 ESD protection
212 U7,U8 2 TPS259470A Texas Instruments TPS259470ARPWR board:Texas_RPW0010A_QFN10 SMT no True-reverse-blocking eFuse; UV/OV/current/inrush protection
213 U9 1 TPS2121 Texas Instruments TPS2121RUXR board:Texas_RUX0012A_VQFN12 SMT no 4.5-A power mux; external input wins near-equal valid 5-V inputs 4.5-A power mux. PR1=GND disables priority mode; CP2 tied to VCC3V3_AUX is what selects IN2 (external 5 V) once the aux rail is up
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,4 +1,4 @@
ERC report (2026-08-15T18:09:04, Encoding UTF8) ERC report (2026-09-01T13:26:17, Encoding UTF8)
Report includes: Errors, Warnings, Exclusions Report includes: Errors, Warnings, Exclusions
***** Sheet / ***** Sheet /
+5 -2
View File
@@ -1,5 +1,5 @@
Comment,Designator,Footprint,LCSC Part #,Manufacturer,Manufacturer Part Number,Quantity,Assembly Type,DNP Comment,Designator,Footprint,LCSC Part #,Manufacturer,Manufacturer Part Number,Quantity,Assembly Type,DNP
100nF,C1,Capacitor_SMD:C_0402_1005Metric,,Murata,GRM155R71A104KA01D,1,SMT,no 100nF,"C1,C129",Capacitor_SMD:C_0402_1005Metric,,Murata,GRM155R71A104KA01D,2,SMT,no
47uF,"C10,C11",Capacitor_SMD:C_1210_3225Metric,,Murata,GRM32ER70J476ME19L,2,SMT,no 47uF,"C10,C11",Capacitor_SMD:C_1210_3225Metric,,Murata,GRM32ER70J476ME19L,2,SMT,no
100nF,C100,Capacitor_SMD:C_0402_1005Metric,,Murata,GRM155R71A104KA01D,1,SMT,no 100nF,C100,Capacitor_SMD:C_0402_1005Metric,,Murata,GRM155R71A104KA01D,1,SMT,no
1uF,C101,Capacitor_SMD:C_0402_1005Metric,,Murata,GRM155R60J105KE19D,1,SMT,no 1uF,C101,Capacitor_SMD:C_0402_1005Metric,,Murata,GRM155R60J105KE19D,1,SMT,no
@@ -85,6 +85,7 @@ GPS L1 SMA,J2,Connector_Coaxial:SMA_Amphenol_132289_EdgeMount,,Amphenol RF,13228
EXT_5V,J3,"TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-2_1x02_P5.00mm_Horizontal",,Phoenix Contact,1935161,1,THT,no EXT_5V,J3,"TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-2_1x02_P5.00mm_Horizontal",,Phoenix Contact,1935161,1,THT,no
EXT RESET_N,J4,Connector_JST:JST_SH_SM02B-SRSS-TB_1x02-1MP_P1.00mm_Horizontal,,JST,SM02B-SRSS-TB(LF)(SN),1,SMT,no EXT RESET_N,J4,Connector_JST:JST_SH_SM02B-SRSS-TB_1x02-1MP_P1.00mm_Horizontal,,JST,SM02B-SRSS-TB(LF)(SN),1,SMT,no
ANT BIAS OFF/3V3/5V,J5,Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical,,Wuerth Elektronik,61300311121,1,THT,no ANT BIAS OFF/3V3/5V,J5,Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical,,Wuerth Elektronik,61300311121,1,THT,no
USB-C power input,J7,Connector_USB:USB_C_Receptacle_GCT_USB4105-xx-A_16P_TopMnt_Horizontal,,GCT,USB4105-GF-A-120,1,SMT,no
PMOD 3.3 V,"JPMOD1,JPMOD2",board:Digilent_PMOD_2x6_P2.54mm,,Amphenol ICC,67997-412HLF,2,THT,no PMOD 3.3 V,"JPMOD1,JPMOD2",board:Digilent_PMOD_2x6_P2.54mm,,Amphenol ICC,67997-412HLF,2,THT,no
PMOD 1.8 V,"JPMOD3,JPMOD4",board:Digilent_PMOD_2x6_P2.54mm,,Amphenol ICC,67997-412HLF,2,THT,no PMOD 1.8 V,"JPMOD3,JPMOD4",board:Digilent_PMOD_2x6_P2.54mm,,Amphenol ICC,67997-412HLF,2,THT,no
100nH,L1,board:Coilcraft_XEL4030,,Coilcraft,XEL4030-101MEC,1,SMT,no 100nH,L1,board:Coilcraft_XEL4030,,Coilcraft,XEL4030-101MEC,1,SMT,no
@@ -100,6 +101,7 @@ MMBT3904,Q1,Package_TO_SOT_SMD:SOT-23,,onsemi,MMBT3904LT1G,1,SMT,no
10K,R104,Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-0710KL,1,SMT,no 10K,R104,Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-0710KL,1,SMT,no
10K,R105,Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-0710KL,1,SMT,no 10K,R105,Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-0710KL,1,SMT,no
10K,R106,Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-0710KL,1,SMT,no 10K,R106,Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-0710KL,1,SMT,no
10K,R108,Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-0710KL,1,SMT,no
232K,"R11,R16",Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-07232KL,2,SMT,no 232K,"R11,R16",Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-07232KL,2,SMT,no
100K,"R12,R17",Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-07100KL,2,SMT,no 100K,"R12,R17",Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-07100KL,2,SMT,no
383K,"R13,R18",Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-07383KL,2,SMT,no 383K,"R13,R18",Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-07383KL,2,SMT,no
@@ -134,7 +136,7 @@ MMBT3904,Q1,Package_TO_SOT_SMD:SOT-23,,onsemi,MMBT3904LT1G,1,SMT,no
100K,R55,Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-07100KL,1,SMT,no 100K,R55,Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-07100KL,1,SMT,no
33R,R56,Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-0733RL,1,SMT,no 33R,R56,Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-0733RL,1,SMT,no
22R,"R57,R58,R59",Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-0722RL,3,SMT,no 22R,"R57,R58,R59",Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-0722RL,3,SMT,no
900K,R6,Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-07909KL,1,SMT,no 900K,"R6,R107",Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-07909KL,2,SMT,no
22R,R60,Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-0722RL,1,SMT,no 22R,R60,Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-0722RL,1,SMT,no
10K,R61,Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-0710KL,1,SMT,no 10K,R61,Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-0710KL,1,SMT,no
27R,R62,Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-0727RL,1,SMT,no 27R,R62,Resistor_SMD:R_0402_1005Metric,,Yageo,RC0402FR-0727RL,1,SMT,no
@@ -204,6 +206,7 @@ SN74LVC1G07,U31,Package_TO_SOT_SMD:SOT-23-5,,Texas Instruments,SN74LVC1G07DBVR,1
SN74LVC1G07,U32,Package_TO_SOT_SMD:SOT-23-5,,Texas Instruments,SN74LVC1G07DBVR,1,SMT,no SN74LVC1G07,U32,Package_TO_SOT_SMD:SOT-23-5,,Texas Instruments,SN74LVC1G07DBVR,1,SMT,no
SN74LVC1G07,U33,Package_TO_SOT_SMD:SOT-23-5,,Texas Instruments,SN74LVC1G07DBVR,1,SMT,no SN74LVC1G07,U33,Package_TO_SOT_SMD:SOT-23-5,,Texas Instruments,SN74LVC1G07DBVR,1,SMT,no
SN74LVC08A,U34,Package_SO:TSSOP-14_4.4x5mm_P0.65mm,,Texas Instruments,SN74LVC08APWR,1,SMT,no SN74LVC08A,U34,Package_SO:TSSOP-14_4.4x5mm_P0.65mm,,Texas Instruments,SN74LVC08APWR,1,SMT,no
TUSB320LAI,U35,Package_DFN_QFN:Texas_X2QFN-12_1.6x1.6mm_P0.4mm,,Texas Instruments,TUSB320LAIRSW(R),1,SMT,no
TUSB320LAI,U5,Package_DFN_QFN:Texas_X2QFN-12_1.6x1.6mm_P0.4mm,,Texas Instruments,TUSB320LAIRSW(R),1,SMT,no TUSB320LAI,U5,Package_DFN_QFN:Texas_X2QFN-12_1.6x1.6mm_P0.4mm,,Texas Instruments,TUSB320LAIRSW(R),1,SMT,no
TPD2EUSB30A,U6,Package_TO_SOT_SMD:Texas_DRT-3,,Texas Instruments,TPD2EUSB30ADRTR,1,SMT,no TPD2EUSB30A,U6,Package_TO_SOT_SMD:Texas_DRT-3,,Texas Instruments,TPD2EUSB30ADRTR,1,SMT,no
TPS259470A,"U7,U8",board:Texas_RPW0010A_QFN10,,Texas Instruments,TPS259470ARPWR,2,SMT,no TPS259470A,"U7,U8",board:Texas_RPW0010A_QFN10,,Texas Instruments,TPS259470ARPWR,2,SMT,no
1 Comment Designator Footprint LCSC Part # Manufacturer Manufacturer Part Number Quantity Assembly Type DNP
2 100nF C1 C1,C129 Capacitor_SMD:C_0402_1005Metric Murata GRM155R71A104KA01D 1 2 SMT no
3 47uF C10,C11 Capacitor_SMD:C_1210_3225Metric Murata GRM32ER70J476ME19L 2 SMT no
4 100nF C100 Capacitor_SMD:C_0402_1005Metric Murata GRM155R71A104KA01D 1 SMT no
5 1uF C101 Capacitor_SMD:C_0402_1005Metric Murata GRM155R60J105KE19D 1 SMT no
85 EXT_5V J3 TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-2_1x02_P5.00mm_Horizontal Phoenix Contact 1935161 1 THT no
86 EXT RESET_N J4 Connector_JST:JST_SH_SM02B-SRSS-TB_1x02-1MP_P1.00mm_Horizontal JST SM02B-SRSS-TB(LF)(SN) 1 SMT no
87 ANT BIAS OFF/3V3/5V J5 Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical Wuerth Elektronik 61300311121 1 THT no
88 USB-C power input J7 Connector_USB:USB_C_Receptacle_GCT_USB4105-xx-A_16P_TopMnt_Horizontal GCT USB4105-GF-A-120 1 SMT no
89 PMOD 3.3 V JPMOD1,JPMOD2 board:Digilent_PMOD_2x6_P2.54mm Amphenol ICC 67997-412HLF 2 THT no
90 PMOD 1.8 V JPMOD3,JPMOD4 board:Digilent_PMOD_2x6_P2.54mm Amphenol ICC 67997-412HLF 2 THT no
91 100nH L1 board:Coilcraft_XEL4030 Coilcraft XEL4030-101MEC 1 SMT no
101 10K R104 Resistor_SMD:R_0402_1005Metric Yageo RC0402FR-0710KL 1 SMT no
102 10K R105 Resistor_SMD:R_0402_1005Metric Yageo RC0402FR-0710KL 1 SMT no
103 10K R106 Resistor_SMD:R_0402_1005Metric Yageo RC0402FR-0710KL 1 SMT no
104 10K R108 Resistor_SMD:R_0402_1005Metric Yageo RC0402FR-0710KL 1 SMT no
105 232K R11,R16 Resistor_SMD:R_0402_1005Metric Yageo RC0402FR-07232KL 2 SMT no
106 100K R12,R17 Resistor_SMD:R_0402_1005Metric Yageo RC0402FR-07100KL 2 SMT no
107 383K R13,R18 Resistor_SMD:R_0402_1005Metric Yageo RC0402FR-07383KL 2 SMT no
136 100K R55 Resistor_SMD:R_0402_1005Metric Yageo RC0402FR-07100KL 1 SMT no
137 33R R56 Resistor_SMD:R_0402_1005Metric Yageo RC0402FR-0733RL 1 SMT no
138 22R R57,R58,R59 Resistor_SMD:R_0402_1005Metric Yageo RC0402FR-0722RL 3 SMT no
139 900K R6 R6,R107 Resistor_SMD:R_0402_1005Metric Yageo RC0402FR-07909KL 1 2 SMT no
140 22R R60 Resistor_SMD:R_0402_1005Metric Yageo RC0402FR-0722RL 1 SMT no
141 10K R61 Resistor_SMD:R_0402_1005Metric Yageo RC0402FR-0710KL 1 SMT no
142 27R R62 Resistor_SMD:R_0402_1005Metric Yageo RC0402FR-0727RL 1 SMT no
206 SN74LVC1G07 U32 Package_TO_SOT_SMD:SOT-23-5 Texas Instruments SN74LVC1G07DBVR 1 SMT no
207 SN74LVC1G07 U33 Package_TO_SOT_SMD:SOT-23-5 Texas Instruments SN74LVC1G07DBVR 1 SMT no
208 SN74LVC08A U34 Package_SO:TSSOP-14_4.4x5mm_P0.65mm Texas Instruments SN74LVC08APWR 1 SMT no
209 TUSB320LAI U35 Package_DFN_QFN:Texas_X2QFN-12_1.6x1.6mm_P0.4mm Texas Instruments TUSB320LAIRSW(R) 1 SMT no
210 TUSB320LAI U5 Package_DFN_QFN:Texas_X2QFN-12_1.6x1.6mm_P0.4mm Texas Instruments TUSB320LAIRSW(R) 1 SMT no
211 TPD2EUSB30A U6 Package_TO_SOT_SMD:Texas_DRT-3 Texas Instruments TPD2EUSB30ADRTR 1 SMT no
212 TPS259470A U7,U8 board:Texas_RPW0010A_QFN10 Texas Instruments TPS259470ARPWR 2 SMT no
+21
View File
@@ -0,0 +1,21 @@
Board: hardware/board_routed.kicad_pcb
Unconnected endpoints reported by connectivity: 0
Tracks: 3067 Vias: 1020
Track segments per layer:
F.Cu 1747
L6_SIG 482
L5_SIG 390
L3_SIG 387
B.Cu 61
Track width used per net class (mm: segments):
BGA_ESCAPE 0.1: 423
CLOCK 0.1: 17, 0.15: 145
Default 0.1: 37, 0.12: 1375, 0.15: 5
POWER 0.1: 79, 0.12: 4, 0.15: 193, 0.163: 18, 0.195: 1, 0.2: 4, 0.22: 12, 0.227: 3, 0.25: 248, 0.26: 6, 0.3: 327, 0.35: 1, 0.4: 103, 0.45: 2, 0.5: 2, 0.55: 10, 0.6: 4, 0.7: 2, 0.8: 5, 1.0: 1, 1.2: 4, 1.5: 2
RF50_CPWG 0.164: 10
USB90_DIFF 0.135: 24
Nets still split into more than one copper group:
none - every net is one connected group
+191 -31
View File
@@ -60,70 +60,225 @@ beklenmez.
Ti375'in bağlı ball'larına kilitli 0.8 mm pitch dogbone/via escape ekler. Ti375'in bağlı ball'larına kilitli 0.8 mm pitch dogbone/via escape ekler.
### 2. Yerel plane bağlantıları ### 2. Güç katı
```bash
/usr/bin/python3 scripts/route_power_stage.py \
hardware/board_prerouted.kicad_pcb \
hardware/board_power_routed.kicad_pcb
```
Autorouter'dan önce aşağıdaki yüksek akım hatlarını deterministik ve kilitli
olarak route eder:
- TPS6287B15 giriş kapasitörü, SW, indüktör, çıkış kapasitörü ve remote-sense.
- TPS62867 1.8 V / 3.3 V giriş-SW-çıkış döngüleri ve plane via dizileri.
- Converter bölgesindeki SYS5 dağıtımı ve ana güç geçişleri.
SW node bakırı küçük tutulmalı; feedback/sense hatları SW ve indüktörden uzak,
Kelvin bağlantılı götürülmelidir. Autorouter bu kararları vermez.
Bu aşama stitching'den **önce** gelir. Tersi sırada plane stitching, buck
çıkışının geçmesi gereken koridora via bırakıp yüksek akım yolunu kesiyordu.
### 3. Yerel plane bağlantıları
```bash ```bash
/usr/bin/python3 scripts/stitch_plane_pads.py \ /usr/bin/python3 scripts/stitch_plane_pads.py \
hardware/board_prerouted.kicad_pcb \ hardware/board_power_routed.kicad_pcb \
hardware/board_plane_stitched.kicad_pcb hardware/board_plane_stitched.kicad_pcb
``` ```
Yalnızca altında gerçekten aynı nete ait dolu plane bulunan SMD güç/GND Yalnızca altında gerçekten aynı nete ait dolu plane bulunan SMD güç/GND
padlerini kısa dogbone ile bağlar. Bu işlem yüksek akım buck loop routinginin padlerini kısa dogbone ile bağlar. Bu işlem yüksek akım buck loop routinginin
yerini tutmaz. yerini tutmaz. RF/USB geçici keepoutlarına through-via koymaz; edge-launch J2
ground bağlantıları final RF ground zone/via-fence aşamasına bırakılır.
### 3. Güç katı ### 4. Yerel güç tamamlama
KiCad içinde önce aşağıdaki hatlar elle route edilmelidir: ```bash
/usr/bin/python3 scripts/route_local_power.py \
hardware/board_plane_stitched.kicad_pcb \
hardware/board_power_local.kicad_pcb --clocks
```
- TPS6287B15 giriş kapasitörü, SW, indüktör, çıkış kapasitörü ve remote-sense. `--clocks` clock sınıfını da burada, autorouter başlamadan kapatır; böylece
- TPS62867 1.8 V ve 3.3 V converter loopları. saat hatları sabit bakır olarak DSN'e girer ve router onları bozamaz.
- SYS5 dağıtımı ve ana güç geçişleri.
- LDO giriş/çıkış bypass bağlantıları.
SW node bakırı küçük tutulmalı; feedback/sense hatları SW ve indüktörden uzak, Plane ve plane stitching'in ulaşamadığı güç bağlantılarını tamamlar: adasının
Kelvin bağlantılı götürülmelidir. Autorouter bu kararları vermez. hemen dışında kalan pad'ler, plane'i olmayan küçük raylar (`FTDI_1V8`,
`IMU_VREGD`, anten bias hatları) ve her LDO'nun etrafındaki yerel giriş/çıkış
döngüleri. Gerçek connectivity grafiği üzerinde çalışır: ayrık kalan her grup
için ya kendi plane'ine via indirir ya da en yakın gruba 45 derecelik yol
çeker. Bütün sonuçlar kilitlidir.
### 4. Freerouting için sınıf bazlı DSN İki fazlıdır. Önce bütün raylar için yalnızca plane inişi ve kısa doğrudan
yollar denenir; ikinci fazda kalanlar ara nokta üzerinden dolaşabilir. Bu ayrım
olmadan listedeki ilk ray, sonraki bir rayın kısa bağlantı için ihtiyaç duyduğu
boşluğu uzun yollarla harcar.
Bir grubu kendi plane'ine indirmek, o adada başka grup yoksa grup sayısını
düşürmez. Bu yüzden ilerleme, `reach_plane`'in döndürdüğü değere değil grup
sayısının gerçekten azalmasına bakılarak ölçülür ve zaten plane'e bağlı gruplar
atlanır. Aksi halde aynı pad çevresinde her turda 0.4 mm daha dışarıda yeni bir
via açılır ve ray tek bir pad'in etrafında onlarca gereksiz via'dan oluşan bir
"via cloud" ile biter.
Bu aşama güç raylarını autorouter'a bırakmamanın karşılığıdır. Freerouting
DSN'de plane göremez; `GND` ve `VCC3V3` gibi rayları ona verseydik yüzlerce
pad'i plane yerine trace ile birbirine bağlamaya çalışırdı.
### 5. Freerouting için sınıf bazlı DSN
```bash ```bash
/usr/bin/python3 scripts/export_routing_stage.py \ /usr/bin/python3 scripts/export_routing_stage.py \
hardware/board_plane_stitched.kicad_pcb outputs/signal_stage.dsn \ hardware/board_power_local.kicad_pcb outputs/signal_stage.dsn \
--classes "Default;BGA_ESCAPE;CLOCK" \ --classes "Default;BGA_ESCAPE;CLOCK" \
--preview outputs/signal_stage_preview.kicad_pcb --extra-nets "ANT_BIAS_SW;EXT5_PROT;FTDI_1V8;FTDI_VPHY;FTDI_VPLL;USB5_PROT;VBUS_RAW" \
--fixed-copper keepout
python3 scripts/prepare_freerouting_dsn.py \
outputs/signal_stage.dsn outputs/signal_stage_freerouting.dsn
``` ```
İkinci script Freerouting 2.3.0/KiCad 10 DSN clearance ölçek farkını düzeltir; `--extra-nets`, POWER sınıfında olduğu için sınıf filtresine takılan ama
koordinatlara, track genişliklerine, via veya keepoutlara dokunmaz. Freerouting plane'i de bulunmayan rayları listeler; yerel tamamlama bunlardan artakalan
arayüzünde düzeltilmiş DSN açılır, en fazla 8 thread kullanılır ve sonuç `.ses` bağlantıları autorouter'a devreder.
olarak kaydedilir.
RF, USB, güç converter loopları ve hassas clock kararları otomatik sonuca Route edilmeyen netlerin bakırı `--fixed-copper keepout` ile her parça için bir
bırakılmamalıdır. `to route` sayısının uzun süre düşmemesi halinde aynı işlemi rule area olarak aktarılır: router o bakırı görmez, yalnızca etrafından dolaşır.
saatlerce sürdürmek yerine placement/escape/keepout çatışması incelenmelidir. `--fixed-copper wire` alternatifi bakırı kendi netinde `(type fix)` wire olarak
yazar ve netleri network bölümünde pinsiz tanımlar; daha az alan üretir ama bu
kartta pass başına yaklaşık dört kat yavaş çalışır.
### 5. Kritik hatları kur ve session'ı sonlandır Buradaki alan sayısı doğrudan yerel güç aşamasının ne kadar bakır bıraktığına
bağlıdır: via cloud'lar temizlendikten sonra 4146 keepout 2541'e düştü.
Bu aşamanın autorouter'ı hâlâ **Freerouting 2.3.0**'dır (aşağıda). KiCad 10 DSN
clearance değerlerini doğru ölçekte okur; `-mt 1` ile çalıştırılır (çok-thread
optimizasyon clearance ihlali üretebiliyor).
`hardware/power_routes.json` ve `hardware/critical_routes.json` el çizimi
geometriyi veri olarak tutar (bkz. `hardware/routes_schema.md`); tek seferlik
üretim: `/usr/bin/python3 scripts/_extract_fixed_routes.py`.
> Bu repoda bir zamanlar `router/` altında bağımsız bir C++ autorouter +
> Qt GUI (`pcbrouter`) vardı. Yoğun 8 katmanlı sinyal aşamasında Freerouting
> kadar tamamlamadığı için kartın routing akışında hiç kullanılmadı ve
> 2026-09-01'de repodan çıkarıldı (`../pcbrouter-cpp-20260901.tar.gz` olarak
> arşivlendi). Sinyal aşaması Freerouting 2.3.0, kalan bağlantılar
> `close_remaining.py` ile kapatılır.
```bash
.tools/freerouting-2.3.0/app/freerouting-2.3.0-linux-x64/bin/freerouting \
-de outputs/signal_stage.dsn -do outputs/signal_stage_pass4.ses \
-mp 4 -mt 1 -us Greedy -is prioritized
```
Bu tasarımda Freerouting'in 20-25 bağlantıda plato yapması beklenen durumdur;
onu sıfıra ulaşana kadar açık bırakmak session'ın hiç yazılmamasına yol açar.
`-mp 4` global yoğun rotayı sınırlar ve kalan bağlantılar varken de geçerli SES
yazar. İnce BGA/FTDI çıkışları, mevcut dogbone ve track uçlarını anchor olarak
kullanan KiCad-side finisher tarafından sonraki adımda tamamlanır. Böylece SES
kaydı için sıfır unrouted önkoşulu yoktur; sıfır bağlantı ve sıfır DRC yalnızca
son KiCad kontrolünün kabul ölçütüdür.
RF, USB ve güç converter loopları otomatik sonuca bırakılmaz. Bu netler DSN'de
hiç yoktur; yerlerini `generate_pcb.py` içindeki `TEMP_` rule area'ları tutar.
`TEMP_*_BELOW` alanları RF ve USB altındaki referans/routing katmanlarını,
`TEMP_RF_LINE` ve `TEMP_USB_*` koridorları ise `finalize_routing.py`'nin
kuracağı geometrinin F.Cu izini korur. CLOCK sınıfı kendi genişlik/clearance
kurallarıyla route edilir ve finalde ayrıca incelenir.
### 6. Kritik hatları kur ve session'ı sonlandır
```bash ```bash
/usr/bin/python3 scripts/finalize_routing.py \ /usr/bin/python3 scripts/finalize_routing.py \
outputs/signal_stage.ses \ outputs/signal_stage_pass4.ses \
--output hardware/board_routed.kicad_pcb --base hardware/board_power_local.kicad_pcb \
--output hardware/board_routed.kicad_pcb \
--finish-nets auto
``` ```
Bu işlem Freerouter'ın RF ve USB üzerindeki copperını kaldırır, denetlenmiş Bu işlem Freerouter'ın RF ve USB üzerindeki copperını kaldırır, geçici rule
RF/USB geometrisini ekler, RF GND via fence'i kurar ve zone'ları doldurur. area'ları siler, denetlenmiş RF/USB geometrisini ekler ve RF GND via fence'i
kurar. Ardından koridorlar boşaldığı için `stitch_plane_pads` ve
`route_local_power` bir kez daha çalıştırılır; bu, RF/USB bölgesindeki güç
pad'lerinin plane'e ulaşabildiği tek andır. Kalan CLOCK, BGA_ESCAPE, Default ve
yalnız düşük-akım yerel POWER netleri `finish_signal_routes.py` ile kapatılır;
5-V giriş yolları ile ana FPGA rayları dar fallback route'a kesinlikle verilmez.
Son olarak dangling copper temizlenir, plane'lerde island temizliği açılır ve
zone'lar doldurulur.
### 6. Final kontroller ### 7. Kalan bağlantıları kapat
```bash
/usr/bin/python3 scripts/close_remaining.py \
hardware/board_routed.kicad_pcb --deadline 60 --ripup-depth 2
```
Freerouting ve yerel tamamlama adımlarından sonra kalan her açık bağlantıyı tek
bir çok katmanlı maze router ile kapatır. Bu adımın yerini aldığı eski
artık-bağlantı scriptlerinin sabit bir şekli vardı: A pad'inin yanına via, tek
bir katmanda A*, B pad'inin yanına via. Bu şekil, dört dogbone noktası da dolu
olan bir 0.8 mm BGA balını kaçıramaz ve sinyal hatlarının adalara böldüğü bir
güç pour'unu köprüleyemez. Bu kartta kalan başarısızlıklar tam olarak bunlardı,
bu yüzden o scriptler kaldırıldı.
`close_remaining.py`:
- Beş route edilebilir katmanın hepsinde `(x, y, katman)` üzerinde 0.1 mm
adımla arar; via'nın yasal olarak sığdığı her yerde katman değiştirir.
- Başlangıç ve hedef, birleştirilen iki grubun **bütün** bakırıdır (pad, track,
via); tek bir pad değil.
- Clearance'ı gerçek pad/track/via geometrisinden, gerçek net-class
değerleriyle hesaplar.
- Bakırı U1 courtyard'ı içinde 0.10 mm'ye daraltır (iki 0.8 mm land arasından
geçen tek genişlik), dışarıda net-class genişliğini kullanır.
- Yabancı bir güç pour'unun içine girmez; böylece yeni bir hat bir güç adasını
ikiye bölemez.
- Sığmayan bir bağlantı için autorouter bakırını **geri alınabilir şekilde**
söker: hattı geçirir, yerinden ettiği netleri tekrar route eder, biri geri
konamazsa her şeyi eski haline döndürür. Rip-up kartı asla bulduğundan daha
kötü bırakmaz.
Eklenen her şey kilitlidir. Sonrasında zone'lar doldurulur ve dangling bakır
temizlenir.
#### Bu adımın gerektirdiği iki kural değişikliği
**1. `Ti375 escape field copper clearance` (yeni, `board.kicad_dru`).**
0.8 mm ball pitch, iki komşu 0.45 mm dogbone via arasında 0.35 mm'lik bir
kanal bırakır. 0.10 mm'lik bir hat bu kanala escape alanının 0.10 mm bakır
minimumunda sığar, CLOCK sınıfının 0.15 mm'sinde sığmaz (0.10 + 2x0.15 = 0.40
> 0.35). Bu yüzden `HARD_TCK` ve `MAX_SCLK` paketten hiç çıkarılamıyordu. Yeni
kural, **her iki nesne de** U1 courtyard'ının içindeyken clearance'ı bu alanın
geri kalanının zaten kullandığı 0.10 mm'ye indirir. Clock trunk, U1
courtyard'ından çıkar çıkmaz 0.15 mm'sine geri döner.
**2. `RF_NO_DIGITAL` artık RF hattının koridoru kadar.**
Alan (152, 36)-(174.5, **49.5**) dikdörtgeniydi; RF bakırı ise y <= 42.6'da
kalıyor. Yani alan, RF hattının 7 mm güneyine kadar uzanıyor ve MAX2771'in
bütün dijital pin sıralarını (y = 44.5 .. 47.4) içine alıyordu. Sonuç: U21'in
her dijital çıkışı yalnız F.Cu'dan kaçmak zorundaydı ve sığmıyordu. Alan
(152, 36)-(174.5, **43.7**) yapıldı; bu, `generate_pcb.py`'nin `TEMP_RF_LINE`
olarak zaten ayırdığı koridorun aynısıdır ve RF bakırını 1.1 mm açıkla geçer.
RF dönüş yolu zaten bu alan tarafından değil, üzerinde hiç route edilmeyen
L2/L7 GND plane'leri tarafından garanti edilir.
### 8. Final kontroller
```bash ```bash
kicad-cli pcb drc --severity-all --schematic-parity --refill-zones \ kicad-cli pcb drc --severity-all --schematic-parity --refill-zones \
--save-board -o outputs/drc_final_routed.rpt \ --save-board -o outputs/drc_final_routed.rpt \
hardware/board_routed.kicad_pcb hardware/board_routed.kicad_pcb
/usr/bin/python3 scripts/report_routing.py hardware/board_routed.kicad_pcb
``` ```
DRC "yanlış olan var mı" sorusunu, `report_routing.py` ise "eksik olan var mı
ve bakır kurallarına uyuyor mu" sorusunu yanıtlar: hâlâ birden fazla parçaya
bölünmüş netleri, net-class başına gerçekten kullanılan hat genişliğini
(Specctra gidiş-dönüşünde sessizce değişebilen değer) ve katman dağılımını
listeler.
Final kartta unconnected item, short, clearance, isolated copper veya Final kartta unconnected item, short, clearance, isolated copper veya
schematic-parity hatası kabul edilmez. RF uzunlukları, USB eşleşmesi, güç schematic-parity hatası kabul edilmez. RF uzunlukları, USB eşleşmesi, güç
loopları, return path ve test-point erişimi görsel olarak da incelenmelidir. loopları, return path ve test-point erişimi görsel olarak da incelenmelidir.
@@ -139,6 +294,11 @@ loopları, return path ve test-point erişimi görsel olarak da incelenmelidir.
| `generate_pcb.py` | Stackup, rule, zone ve deterministic placement üretimi | | `generate_pcb.py` | Stackup, rule, zone ve deterministic placement üretimi |
| `preroute_bga.py` | Kilitli Ti375 dogbone escape | | `preroute_bga.py` | Kilitli Ti375 dogbone escape |
| `stitch_plane_pads.py` | Yerel pad-to-plane bağlantıları | | `stitch_plane_pads.py` | Yerel pad-to-plane bağlantıları |
| `route_power_stage.py` | Buck yüksek-akım döngüleri ve plane girişleri |
| `route_local_power.py` | Plane'e ulaşamayan güç bağlantılarının deterministik tamamlanması |
| `export_routing_stage.py` | Seçili net-class için DSN üretimi | | `export_routing_stage.py` | Seçili net-class için DSN üretimi |
| `prepare_freerouting_dsn.py` | Freerouting 2.3 clearance düzeltmesi | | `prepare_freerouting_dsn.py` | Yalnız doğrulanmış farklı router build'leri için opsiyonel DSN clearance ölçekleme |
| `finalize_routing.py` | SES importu ve denetlenmiş RF/USB geometrisi | | `finalize_routing.py` | SES importu ve denetlenmiş RF/USB geometrisi |
| `finish_signal_routes.py` | Global router'ın bıraktığı yerel bağlantıları gerçek KiCad obstacle modeliyle tamamlar |
| `close_remaining.py` | Kalan bütün açık bağlantıları çok katmanlı maze + geri alınabilir rip-up ile kapatır |
| `report_routing.py` | Routed kartta eksik bağlantı, hat genişliği ve katman dağılımı raporu |
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env python3
"""One-time generator: capture the hand-drawn geometry from route_power_stage.py
and finalize_routing.py into hardware/power_routes.json and
hardware/critical_routes.json, which the project-independent `pcbrouter fixed`
loader consumes.
This is a development tool, not part of the routing flow. It uses pcbnew only to
resolve the pad anchors the scripts reference; the emitted JSON is plain data.
The JSON files are already committed; you only need this to regenerate them after
changing route_power_stage.py / finalize_routing.py.
It reads the intermediate boards board_prerouted.kicad_pcb and
board_power_local.kicad_pcb, which are git-ignored pipeline artifacts. If they
are not present, regenerate them first by running steps 1-4 of scripts/README.md
(preroute_bga -> route_power_stage -> stitch_plane_pads -> route_local_power).
/usr/bin/python3 scripts/_extract_fixed_routes.py
"""
from __future__ import annotations
import json
from pathlib import Path
import pcbnew
ROOT = Path(__file__).resolve().parents[1]
def mm(v: int) -> float:
return round(pcbnew.ToMM(v), 4)
class Capture:
def __init__(self) -> None:
self.tracks: list[dict] = []
self.vias: list[dict] = []
def track(self, net, a, b, width, layer_name):
if a == b:
return
self.tracks.append({
"net": net, "layer": layer_name, "width": round(width, 4),
"points": [[round(a[0], 4), round(a[1], 4)], [round(b[0], 4), round(b[1], 4)]],
})
def via(self, net, x, y, dia=0.45, drill=0.225):
self.vias.append({"net": net, "at": [round(x, 4), round(y, 4)],
"diameter": round(dia, 4), "drill": round(drill, 4)})
def as_json(self) -> dict:
# merge consecutive same-net/same-layer/same-width segments into polylines
merged: list[dict] = []
for t in self.tracks:
if (merged and merged[-1]["net"] == t["net"]
and merged[-1]["layer"] == t["layer"]
and abs(merged[-1]["width"] - t["width"]) < 1e-6
and merged[-1]["points"][-1] == t["points"][0]):
merged[-1]["points"].append(t["points"][1])
else:
merged.append({"net": t["net"], "layer": t["layer"],
"width": t["width"], "points": list(t["points"])})
return {"tracks": merged, "vias": self.vias}
def layer_name(board, layer_id):
return board.GetLayerName(layer_id)
def patch_power_stage(cap: Capture, board):
import route_power_stage as rps
def add_track(b, net, start, end, width, layer=pcbnew.F_Cu):
cap.track(net, start, end, width, layer_name(board, layer))
def add_via(b, net, x, y, diameter=0.70, drill=0.35):
cap.via(net, x, y, diameter, drill)
rps.add_track = add_track
rps.add_via = add_via
rps.add_path = lambda b, net, pts, w, layer=pcbnew.F_Cu: [
add_track(b, net, s, e, w, layer) for s, e in zip(pts, pts[1:])
]
def patch_finalize(cap: Capture, board):
import finalize_routing as fr
def add_segment(b, net, start, end, width, layer=pcbnew.F_Cu, locked=True):
cap.track(net, start, end, width, layer_name(board, layer))
def add_polyline(b, net, verts, width, layer=pcbnew.F_Cu):
for s, e in zip(verts, verts[1:]):
add_segment(b, net, s, e, width, layer)
def add_via(b, net, position, diameter=0.45, drill=0.225):
cap.via(net, position[0], position[1], diameter, drill)
fr.add_segment = add_segment
fr.add_polyline = add_polyline
fr.add_via = add_via
def main() -> None:
for stem in ("board_prerouted", "board_power_local"):
if not (ROOT / "hardware" / f"{stem}.kicad_pcb").exists():
raise SystemExit(
f"hardware/{stem}.kicad_pcb missing - regenerate the pipeline "
"intermediates first (steps 1-4 of scripts/README.md).")
# -- power routes -------------------------------------------------------
board = pcbnew.LoadBoard(str(ROOT / "hardware" / "board_prerouted.kicad_pcb"))
cap = Capture()
patch_power_stage(cap, board)
import route_power_stage as rps
rps.route_core_buck(board)
rps.route_small_buck(board, regulator="U14", inductor="L2",
input_caps=("C26", "C27"), output_caps=("C28", "C29"),
sw_net="BUCK1V8_SW", out_net="VCC1V8",
input_x=112.98, plane_via_x=120.105)
rps.route_small_buck(board, regulator="U15", inductor="L3",
input_caps=("C30", "C31"), output_caps=("C32", "C33"),
sw_net="BUCK3V3_SW", out_net="VCC3V3",
input_x=127.98, plane_via_x=135.105)
(ROOT / "hardware" / "power_routes.json").write_text(
json.dumps(cap.as_json(), indent=1) + "\n")
print(f"power_routes.json: {len(cap.as_json()['tracks'])} polylines, {len(cap.vias)} vias")
# -- critical (RF / USB) routes + rule areas --------------------------
board = pcbnew.LoadBoard(str(ROOT / "hardware" / "board_power_local.kicad_pcb"))
cap = Capture()
patch_finalize(cap, board)
import finalize_routing as fr
fr.route_rf(board)
fr.route_usb(board)
data = cap.as_json()
data["rule_areas"] = [
{"name": "USB_ESD_BREAKOUT", "layers": ["F.Cu"], "rect": [29.90, 50.70, 32.10, 53.10]},
{"name": "RF_NO_DIGITAL", "layers": ["B.Cu", "In2.Cu", "In4.Cu", "In5.Cu"],
"rect": [152.00, 36.00, 174.50, 43.70]},
]
fence = [[x, 38.7] for x in range(153, 174, 2)] + [[x, 45.0] for x in range(161, 174, 2)]
data["ground_zones"] = [{
"net": "GND", "layer": "F.Cu", "clearance": 0.20, "min_thickness": 0.10,
"priority": 100,
"outline": [[152.0, 36.0], [174.5, 36.0], [174.5, 49.5], [152.0, 49.5]],
"via_fence": fence,
}]
(ROOT / "hardware" / "critical_routes.json").write_text(json.dumps(data, indent=1) + "\n")
print(f"critical_routes.json: {len(data['tracks'])} polylines, {len(cap.vias)} vias, "
f"{len(data['rule_areas'])} rule areas, fence {len(fence)}")
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
+65 -11
View File
@@ -156,26 +156,30 @@ for name in ("REF_RES_4A", "REF_RES_4B", "REF_RES_4D", "REF_RES_2E", "REF_RES_2A
r("10K", name, "GND", description="Efinix mandatory 10-kohm 1% reference resistor") r("10K", name, "GND", description="Efinix mandatory 10-kohm 1% reference resistor")
# USB-C and sink role detection. # USB-C debug port (JTAG/UART only). This connector never sources SYS5: its
# VBUS pins only feed local presence-sensing dividers, so a host attached here
# can never be back-fed and dedicated USB power input J7 stays the only USB
# source of board power. See J7 below for the power port and its TUSB320.
add_ic("J1", "USB-C USB2 sink", "USB4105-GF-A-120", "GCT", add_ic("J1", "USB-C USB2 sink", "USB4105-GF-A-120", "GCT",
"Connector_USB:USB_C_Receptacle_GCT_USB4105-xx-A_16P_TopMnt_Horizontal", [ "Connector_USB:USB_C_Receptacle_GCT_USB4105-xx-A_16P_TopMnt_Horizontal", [
p("A1", "GND", "GND"), p("A4", "VBUS", "VBUS_RAW"), p("A5", "CC1", "USB_CC1"), p("A1", "GND", "GND"), p("A4", "VBUS", "DBG_VBUS_RAW"), p("A5", "CC1", "USB_CC1"),
p("A6", "D+", "USB_D_P"), p("A7", "D-", "USB_D_N"), p("A8", "SBU1", None), p("A6", "D+", "USB_D_P"), p("A7", "D-", "USB_D_N"), p("A8", "SBU1", None),
p("A9", "VBUS", "VBUS_RAW"), p("A12", "GND", "GND"), p("A9", "VBUS", "DBG_VBUS_RAW"), p("A12", "GND", "GND"),
p("B1", "GND", "GND"), p("B4", "VBUS", "VBUS_RAW"), p("B5", "CC2", "USB_CC2"), p("B1", "GND", "GND"), p("B4", "VBUS", "DBG_VBUS_RAW"), p("B5", "CC2", "USB_CC2"),
p("B6", "D+", "USB_D_P"), p("B7", "D-", "USB_D_N"), p("B8", "SBU2", None), p("B6", "D+", "USB_D_P"), p("B7", "D-", "USB_D_N"), p("B8", "SBU2", None),
p("B9", "VBUS", "VBUS_RAW"), p("B12", "GND", "GND"), p("SH", "SHIELD", "USB_SHIELD")], p("B9", "VBUS", "DBG_VBUS_RAW"), p("B12", "GND", "GND"), p("SH", "SHIELD", "USB_SHIELD")],
"USB 2.0 Type-C receptacle, UFP/sink") "USB 2.0 Type-C receptacle, UFP/sink, debug data only")
add_ic("U5", "TUSB320LAI", "TUSB320LAIRSW(R)", "Texas Instruments", add_ic("U5", "TUSB320LAI", "TUSB320LAIRSW(R)", "Texas Instruments",
"Package_DFN_QFN:Texas_X2QFN-12_1.6x1.6mm_P0.4mm", [ "Package_DFN_QFN:Texas_X2QFN-12_1.6x1.6mm_P0.4mm", [
p(1, "CC1", "USB_CC1", "bidirectional"), p(2, "CC2", "USB_CC2", "bidirectional"), p(1, "CC1", "USB_CC1", "bidirectional"), p(2, "CC2", "USB_CC2", "bidirectional"),
p(3, "PORT", "GND", "input"), p(4, "VBUS_DET", "USB_VBUS_DET", "input"), p(3, "PORT", "GND", "input"), p(4, "VBUS_DET", "USB_VBUS_DET", "input"),
p(5, "ADDR", None), p(6, "OUT3", None, "output"), p(5, "ADDR", None), p(6, "OUT3", None, "output"),
p(7, "OUT1", "TUSB_OUT1", "output"), p(8, "OUT2", "TUSB_OUT2", "output"), p(7, "OUT1", None, "output"), p(8, "OUT2", None, "output"),
p(9, "ID", None, "output"), p(10, "GND", "GND", "power_in"), p(9, "ID", None, "output"), p(10, "GND", "GND", "power_in"),
p(11, "EN_N", "GND", "input"), p(12, "VDD", "VCC3V3_AUX", "power_in")], p(11, "EN_N", "GND", "input"), p(12, "VDD", "VCC3V3_AUX", "power_in")],
"USB Type-C port controller in UFP GPIO mode; integrated Rd on CC1/CC2") "USB Type-C port controller in UFP GPIO mode; integrated Rd on CC1/CC2; "
r("900K", "VBUS_RAW", "USB_VBUS_DET", description="TUSB320 VBUS_DET current limiter") "current-advertisement outputs unused since this port never sources SYS5")
r("900K", "DBG_VBUS_RAW", "USB_VBUS_DET", description="TUSB320 VBUS_DET current limiter")
r("10K", "VCC3V3_AUX", "TUSB_OUT1", description="TUSB320 open-drain OUT1 pull-up") r("10K", "VCC3V3_AUX", "TUSB_OUT1", description="TUSB320 open-drain OUT1 pull-up")
r("10K", "VCC3V3_AUX", "TUSB_OUT2", description="TUSB320 open-drain OUT2 pull-up") r("10K", "VCC3V3_AUX", "TUSB_OUT2", description="TUSB320 open-drain OUT2 pull-up")
c("100nF", "VCC3V3_AUX", "GND", description="TUSB320 local bypass") c("100nF", "VCC3V3_AUX", "GND", description="TUSB320 local bypass")
@@ -220,13 +224,19 @@ add_ic("U9", "TPS2121", "TPS2121RUXR", "Texas Instruments", "board:Texas_RUX0012
p(4, "OV2", "MUX_OV2", "input"), p(5, "OV1", "MUX_OV1", "input"), p(6, "PR1", "GND", "input"), p(4, "OV2", "MUX_OV2", "input"), p(5, "OV1", "MUX_OV1", "input"), p(6, "PR1", "GND", "input"),
p(7, "IN1", "USB5_PROT", "power_in"), p(8, "OUT", "SYS5", "power_out"), p(9, "ST", "MUX_ST", "output"), p(7, "IN1", "USB5_PROT", "power_in"), p(8, "OUT", "SYS5", "power_out"), p(9, "ST", "MUX_ST", "output"),
p(10, "ILM", "MUX_ILM", "output"), p(11, "SS", "MUX_SS", "output"), p(12, "GND", "GND", "power_in")], p(10, "ILM", "MUX_ILM", "output"), p(11, "SS", "MUX_SS", "output"), p(12, "GND", "GND", "power_in")],
"4.5-A power mux; external input wins near-equal valid 5-V inputs") "4.5-A power mux. PR1=GND disables priority mode; CP2 tied to VCC3V3_AUX "
"is what selects IN2 (external 5 V) once the aux rail is up")
r("442K", "USB5_PROT", "MUX_OV1", description="TPS2121 IN1 5.75-V OV divider top") r("442K", "USB5_PROT", "MUX_OV1", description="TPS2121 IN1 5.75-V OV divider top")
r("100K", "MUX_OV1", "GND", description="TPS2121 IN1 OV divider bottom") r("100K", "MUX_OV1", "GND", description="TPS2121 IN1 OV divider bottom")
r("442K", "EXT5_PROT", "MUX_OV2", description="TPS2121 IN2 5.75-V OV divider top") r("442K", "EXT5_PROT", "MUX_OV2", description="TPS2121 IN2 5.75-V OV divider top")
r("100K", "MUX_OV2", "GND", description="TPS2121 IN2 OV divider bottom") r("100K", "MUX_OV2", "GND", description="TPS2121 IN2 OV divider bottom")
r("22.1K", "MUX_ILM", "GND", description="TPS2121 4.5-A typical output limit") r("22.1K", "MUX_ILM", "GND", description="TPS2121 4.5-A typical output limit")
c("10nF", "MUX_SS", "GND", description="TPS2121 soft-start") c("10nF", "MUX_SS", "GND", description="TPS2121 soft-start")
# ST is open-drain and needs an external 6-20 kohm pull-up (TPS2121 data
# sheet, Recommended Operating Conditions, RST). It is added at the end of
# this file rather than here: reference designators are assigned in call
# order, so inserting a resistor mid-file would renumber every later one and
# invalidate the routed board.
for _ in range(2): for _ in range(2):
c("47uF", "SYS5", "GND", footprint="Capacitor_SMD:C_1210_3225Metric", description="5-V system bulk") c("47uF", "SYS5", "GND", footprint="Capacitor_SMD:C_1210_3225Metric", description="5-V system bulk")
@@ -546,7 +556,7 @@ add_ic("U2", "FT4232H-56Q", "FT4232H-56Q-REEL", "FTDI", "board:FTDI_VQFN56_EP5p9
r("27R", "USB_D_P", "FTDI_D_P", description="FTDI USB D+ series termination") r("27R", "USB_D_P", "FTDI_D_P", description="FTDI USB D+ series termination")
r("27R", "USB_D_N", "FTDI_D_N", description="FTDI USB D- series termination") r("27R", "USB_D_N", "FTDI_D_N", description="FTDI USB D- series termination")
r("12K", "FTDI_REF", "GND", description="FT4232H USB PHY reference resistor, 1%") r("12K", "FTDI_REF", "GND", description="FT4232H USB PHY reference resistor, 1%")
r("4.7K", "VBUS_RAW", "FTDI_RESET_N", description="Self-powered VBUS-present reset divider top") r("4.7K", "DBG_VBUS_RAW", "FTDI_RESET_N", description="Self-powered VBUS-present reset divider top")
r("10K", "FTDI_RESET_N", "GND", description="Self-powered VBUS-present reset divider bottom") r("10K", "FTDI_RESET_N", "GND", description="Self-powered VBUS-present reset divider bottom")
for _ in range(2): for _ in range(2):
c("100nF", "VCC3V3_AUX", "GND", description="FT4232H VCCIO/VREGIN bypass") c("100nF", "VCC3V3_AUX", "GND", description="FT4232H VCCIO/VREGIN bypass")
@@ -718,6 +728,50 @@ add_ic("D2", "RESET ESD", "ESD5Z3.3T1G", "onsemi", "Diode_SMD:D_SOD-523", [
"External-reset connector ESD clamp") "External-reset connector ESD clamp")
# Dedicated USB-C power input, physically and electrically separate from the
# J1 debug/JTAG/UART port. Its VBUS feeds eFuse U7 (unchanged: U7 already
# reads net "VBUS_RAW", which now belongs to this connector instead of J1) so
# it goes through the same reverse-blocking protection as before and arrives
# at the same TPS2121 IN1 input. U9 keeps external EXT5_PROT (J3) as the
# higher-priority source, so external power and this USB-C power input can
# never feed SYS5 at the same time. D+/D-/SBU are power-only and left
# unconnected; the shield ties to the same USB_SHIELD net as J1.
add_ic("J7", "USB-C power input", "USB4105-GF-A-120", "GCT",
"Connector_USB:USB_C_Receptacle_GCT_USB4105-xx-A_16P_TopMnt_Horizontal", [
p("A1", "GND", "GND"), p("A4", "VBUS", "VBUS_RAW"), p("A5", "CC1", "PWR_CC1"),
p("A6", "D+", None), p("A7", "D-", None), p("A8", "SBU1", None),
p("A9", "VBUS", "VBUS_RAW"), p("A12", "GND", "GND"),
p("B1", "GND", "GND"), p("B4", "VBUS", "VBUS_RAW"), p("B5", "CC2", "PWR_CC2"),
p("B6", "D+", None), p("B7", "D-", None), p("B8", "SBU2", None),
p("B9", "VBUS", "VBUS_RAW"), p("B12", "GND", "GND"), p("SH", "SHIELD", "USB_SHIELD")],
"USB 2.0 Type-C receptacle, UFP/sink, power input only")
add_ic("U35", "TUSB320LAI", "TUSB320LAIRSW(R)", "Texas Instruments",
"Package_DFN_QFN:Texas_X2QFN-12_1.6x1.6mm_P0.4mm", [
p(1, "CC1", "PWR_CC1", "bidirectional"), p(2, "CC2", "PWR_CC2", "bidirectional"),
p(3, "PORT", "GND", "input"), p(4, "VBUS_DET", "PWR_USB_VBUS_DET", "input"),
p(5, "ADDR", None), p(6, "OUT3", None, "output"),
p(7, "OUT1", "TUSB_OUT1", "output"), p(8, "OUT2", "TUSB_OUT2", "output"),
p(9, "ID", None, "output"), p(10, "GND", "GND", "power_in"),
p(11, "EN_N", "GND", "input"), p(12, "VDD", "VCC3V3_AUX", "power_in")],
"USB Type-C port controller in UFP GPIO mode for the power input; "
"integrated Rd on CC1/CC2; OUT1/OUT2 3-A advertisement drives U10/U11 MAIN_EN gating")
# TUSB_OUT1/TUSB_OUT2 pull-ups already exist beside U5 (net names are reused,
# not the physical position); adding a second pair here would halve their
# effective value instead of moving them.
r("900K", "VBUS_RAW", "PWR_USB_VBUS_DET", description="TUSB320 VBUS_DET current limiter")
c("100nF", "VCC3V3_AUX", "GND", description="TUSB320 local bypass")
# TPS2121 ST is an open-drain status output: it is driven low only while IN2
# (the external 5 V header) is sourcing OUT, and is high-impedance whenever IN1
# (USB-C) is sourcing or the output is off. U10 gate 2 inverts it into
# EXT_SELECTED, and MAIN_EN = USB_3A_OK OR EXT_SELECTED enables the core buck,
# so without this pull-up the gate input floats on USB-C power and MAIN_EN can
# assert regardless of what the TUSB320 advertised. 10 k is inside the
# datasheet's 6-20 kohm range and matches the other open-drain pull-ups here.
r("10K", "VCC3V3_AUX", "MUX_ST",
description="TPS2121 open-drain status pull-up (RST, 6-20 kohm)")
def validate() -> None: def validate() -> None:
refs = [x.ref for x in components] refs = [x.ref for x in components]
assert len(refs) == len(set(refs)) assert len(refs) == len(set(refs))
+176 -3
View File
@@ -4,11 +4,103 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import re
from pathlib import Path from pathlib import Path
import pcbnew import pcbnew
NET_TOKEN = re.compile(r'\(net\s+("[^"]+"|[^\s()]+)')
RULE_WIDTH = re.compile(r'(\(width\s+)(\d+(?:\.\d+)?)')
# Keepouts describe the copper outline itself and nothing more. Freerouting
# already applies the routing clearance to a keepout boundary, so any envelope
# added here is counted twice: measured against a 0.10-mm envelope it kept
# 0.20 mm, against plain wiring 0.102 mm. In the Ti375 escape field that is
# the difference between a usable 0.385-mm channel and a 0.485-mm one that
# does not exist, and 184 of the 261 escape vias are power nets exported this
# way, so the doubled figure sealed the array shut.
OBSTACLE_CLEARANCE_MM = 0.0
def restore_fixed_net_declarations(dsn: Path) -> list[str]:
"""Declare fixed-copper-only nets without adding their pads to routing."""
source = dsn.read_text()
network_start = source.index(" (network\n")
wiring_start = source.index(" (wiring\n")
network = source[network_start:wiring_start]
wiring = source[wiring_start:]
declared = set(NET_TOKEN.findall(network))
used_by_copper = set(NET_TOKEN.findall(wiring))
missing = sorted(used_by_copper - declared)
if not missing:
return []
declarations = "".join(f" (net {name})\n" for name in missing)
insertion = network_start + len(" (network\n")
dsn.write_text(source[:insertion] + declarations + source[insertion:])
return missing
def inject_obstacles(dsn: Path, obstacles: list[str]) -> None:
"""Insert physical keepouts for copper omitted from the routing nets."""
if not obstacles:
return
source = dsn.read_text()
structure = source.index(" (structure\n")
insertion = source.index(" (via ", structure)
rendered = "".join(f" {item}\n" for item in obstacles)
dsn.write_text(source[:insertion] + rendered + source[insertion:])
def scale_route_rule_widths(dsn: Path, factor: float = 1.0) -> int:
"""Optionally rescale the routed widths the DSN asks Freerouting for.
The default is 1.0 because Freerouting 2.3.0 honours the DSN rule width
exactly: a `(width 120)` rule produces 0.12-mm copper. An earlier 4/3
factor here, meant to compensate a supposed 0.75x output, instead made
every autorouted trace a third too wide, and that is fatal in the Ti375
escape field. Its via grid is on a 0.8-mm pitch, so a trace threading
between two 0.45-mm vias has 0.400 mm to work with: a 0.12-mm trace at
0.10-mm clearance needs 0.385 mm and fits, a 0.16-mm one needs 0.405 mm
and cannot be placed legally at all. Verify with report_routing.py after
every import: the width per net class must equal the net class setting.
"""
source = dsn.read_text()
def scale(match: re.Match[str]) -> str:
value = float(match.group(2)) * factor
rendered = str(int(round(value)))
return match.group(1) + rendered
converted, count = RULE_WIDTH.subn(scale, source)
dsn.write_text(converted)
return count
def enforce_router_clearance_floor(dsn: Path, minimum_um: float = 100) -> int:
"""Hold every routed class to at least the board's minimum copper clearance.
100 um is that minimum, and it is also what the Default and BGA_ESCAPE
classes already ask for, so this normally changes nothing. It must not be
raised to the 0.14 mm of the U1 dogbone rule: that rule governs copper
against Ti375 *lands*, and applying it to via-to-trace spacing as well
closes every channel through the escape field.
"""
source = dsn.read_text()
pattern = re.compile(r'(\(clearance\s+)(\d+(?:\.\d+)?)(\))')
changed = 0
def raise_value(match: re.Match[str]) -> str:
nonlocal changed
value = float(match.group(2))
if value >= minimum_um:
return match.group(0)
changed += 1
return f"{match.group(1)}{minimum_um:g}{match.group(3)}"
dsn.write_text(pattern.sub(raise_value, source))
return changed
def main() -> None: def main() -> None:
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("board", type=Path) parser.add_argument("board", type=Path)
@@ -17,6 +109,20 @@ def main() -> None:
help="Exact KiCad net-class names separated by ';' (or ',' for legacy single names)") help="Exact KiCad net-class names separated by ';' (or ',' for legacy single names)")
parser.add_argument("--preview", type=Path, parser.add_argument("--preview", type=Path,
help="Optional filtered KiCad board for inspection") help="Optional filtered KiCad board for inspection")
parser.add_argument("--fixed-copper", choices=("wire", "keepout"),
default="wire",
help="How copper on non-routed nets is presented to the "
"router (see comment in the source)")
parser.add_argument("--only-nets", default="",
help="Route just these nets, separated by ';'. Every "
"other net becomes fixed copper. Used for a "
"focused second round over the connections the "
"first one left open.")
parser.add_argument("--extra-nets", default="",
help="Additional net names to route, separated by ';'. "
"Used for the few POWER-class rails that have no "
"plane and that route_local_power.py could not "
"finish with a short local path.")
args = parser.parse_args() args = parser.parse_args()
# KiCad can report a compound effective class such as "CLOCK,POWER" when # KiCad can report a compound effective class such as "CLOCK,POWER" when
@@ -27,24 +133,91 @@ def main() -> None:
board = pcbnew.LoadBoard(str(args.board)) board = pcbnew.LoadBoard(str(args.board))
kept_pads = 0 kept_pads = 0
omitted_pads = 0 omitted_pads = 0
kept_nets: set[str] = set() extra = {name.strip() for name in args.extra_nets.split(";") if name.strip()}
only = {name.strip() for name in args.only_nets.split(";") if name.strip()}
kept_nets = {
pad.GetNetname()
for fp in board.GetFootprints()
for pad in fp.Pads()
if pad.GetNetname() and (pad.GetNetClassName() in keep
or pad.GetNetname() in extra)
}
if only:
kept_nets &= only
kept_nets |= only
for fp in board.GetFootprints(): for fp in board.GetFootprints():
for pad in fp.Pads(): for pad in fp.Pads():
if not pad.GetNetname(): if not pad.GetNetname():
continue continue
if pad.GetNetClassName() in keep: if pad.GetNetname() in kept_nets:
kept_pads += 1 kept_pads += 1
kept_nets.add(pad.GetNetname())
else: else:
pad.SetNetCode(0) pad.SetNetCode(0)
omitted_pads += 1 omitted_pads += 1
# Copper on omitted nets must stay a physical obstacle without becoming
# something the router tries to connect. Two encodings are available.
#
# "wire" is the default and much the faster of the two: the copper keeps
# its net, so KiCad exports it as ordinary `(type fix)` wiring, and
# restore_fixed_net_declarations() then declares those nets in the network
# section with no pins. Freerouting therefore has no connection to route
# on them and simply routes around the fixed copper.
#
# "keepout" strips the net and emits an expanded envelope per item instead.
# It is unambiguous but produces thousands of small rule areas, which slows
# every routing pass down considerably. Kept for comparison runs.
obstacles: list[str] = []
# A through via blocks every routing layer, so the layer list has to come
# from the board rather than be written out here: hard-coding it meant that
# promoting L5 from a power plane to a signal layer left every existing via
# invisible on L5, and the router happily drove traces straight through
# them. Freerouting only routes layers it was told are signal layers, so
# that set is exactly the one that needs a keepout.
routing_layers = [layer for layer in board.GetEnabledLayers().CuStack()
if board.GetLayerType(layer) == pcbnew.LT_SIGNAL]
for item in board.GetTracks():
if not item.GetNetname() or item.GetNetname() in kept_nets:
continue
if args.fixed_copper == "wire":
continue
if isinstance(item, pcbnew.PCB_VIA):
diameter = pcbnew.ToMM(item.GetWidth(pcbnew.F_Cu)) + 2 * OBSTACLE_CLEARANCE_MM
position = item.GetPosition()
x, y = pcbnew.ToMM(position.x), -pcbnew.ToMM(position.y)
for layer in routing_layers:
obstacles.append(
f'(keepout "" (circle {board.GetLayerName(layer)} '
f'{diameter * 1000:g} {x * 1000:g} {y * 1000:g}))'
)
else:
start, end = item.GetStart(), item.GetEnd()
width = pcbnew.ToMM(item.GetWidth()) + 2 * OBSTACLE_CLEARANCE_MM
obstacles.append(
f'(keepout "" (path {board.GetLayerName(item.GetLayer())} '
f'{width * 1000:g} {pcbnew.ToMM(start.x) * 1000:g} '
f'{-pcbnew.ToMM(start.y) * 1000:g} {pcbnew.ToMM(end.x) * 1000:g} '
f'{-pcbnew.ToMM(end.y) * 1000:g}))'
)
item.SetNetCode(0)
for zone in list(board.Zones()):
if not zone.GetIsRuleArea():
board.Remove(zone)
board.BuildListOfNets() board.BuildListOfNets()
if args.preview: if args.preview:
pcbnew.SaveBoard(str(args.preview), board) pcbnew.SaveBoard(str(args.preview), board)
if not pcbnew.ExportSpecctraDSN(board, str(args.dsn)): if not pcbnew.ExportSpecctraDSN(board, str(args.dsn)):
raise RuntimeError(f"Failed to export {args.dsn}") raise RuntimeError(f"Failed to export {args.dsn}")
inject_obstacles(args.dsn, obstacles)
scaled_widths = scale_route_rule_widths(args.dsn)
raised_clearances = enforce_router_clearance_floor(args.dsn)
restored = restore_fixed_net_declarations(args.dsn)
print(f"Classes: {', '.join(sorted(keep))}") print(f"Classes: {', '.join(sorted(keep))}")
print(f"Kept {kept_pads} pads on {len(kept_nets)} nets; omitted {omitted_pads} pads") print(f"Kept {kept_pads} pads on {len(kept_nets)} nets; omitted {omitted_pads} pads")
print(f"Fixed copper as {args.fixed_copper}: {len(obstacles)} keepouts inserted")
print(f"Rewrote {scaled_widths} route-rule widths")
print(f"Raised {raised_clearances} route clearances to the board minimum")
print(f"Declared {len(restored)} fixed-copper-only nets")
print(f"Saved {args.dsn}") print(f"Saved {args.dsn}")
+357 -24
View File
@@ -15,8 +15,12 @@ from pathlib import Path
import pcbnew import pcbnew
import finish_signal_routes
import route_local_power
import stitch_plane_pads
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
PLACED = ROOT / "hardware" / "board.kicad_pcb" POWER_LOCAL = ROOT / "hardware" / "board_power_local.kicad_pcb"
ROUTED = ROOT / "hardware" / "board_routed.kicad_pcb" ROUTED = ROOT / "hardware" / "board_routed.kicad_pcb"
RF_NETS = {"RF_SMA", "RF_PRE_SAW", "RF_POST_SAW", "MAX_MIXIN_HI"} RF_NETS = {"RF_SMA", "RF_PRE_SAW", "RF_POST_SAW", "MAX_MIXIN_HI"}
@@ -32,6 +36,20 @@ def point(x: float, y: float) -> pcbnew.VECTOR2I:
return pcbnew.VECTOR2I_MM(x, y) return pcbnew.VECTOR2I_MM(x, y)
def remove(board: pcbnew.BOARD, item) -> None:
"""Remove and disown a board item.
pcbnew's BOARD.Remove() does not clear the SWIG "thisown" flag, so the
Python wrapper still believes it owns the now-freed C++ object. Letting
the garbage collector finalize it later double-frees that memory and
corrupts the process (subsequent pcbnew calls start returning raw,
unusable SwigPyObject instances instead of typed wrappers). Explicitly
disowning the wrapper right after removal avoids that.
"""
board.Remove(item)
item.thisown = False
def footprint(board: pcbnew.BOARD, ref: str) -> pcbnew.FOOTPRINT: def footprint(board: pcbnew.BOARD, ref: str) -> pcbnew.FOOTPRINT:
item = board.FindFootprintByReference(ref) item = board.FindFootprintByReference(ref)
if item is None: if item is None:
@@ -94,7 +112,7 @@ def add_via(board: pcbnew.BOARD, net_name: str, position: tuple[float, float],
def remove_critical_routes(board: pcbnew.BOARD) -> int: def remove_critical_routes(board: pcbnew.BOARD) -> int:
doomed = [item for item in board.GetTracks() if item.GetNetname() in CRITICAL_NETS] doomed = [item for item in board.GetTracks() if item.GetNetname() in CRITICAL_NETS]
for item in doomed: for item in doomed:
board.Remove(item) remove(board, item)
return len(doomed) return len(doomed)
@@ -102,7 +120,7 @@ def remove_temporary_keepouts(board: pcbnew.BOARD) -> int:
doomed = [zone for zone in board.Zones() doomed = [zone for zone in board.Zones()
if zone.GetIsRuleArea() and zone.GetZoneName().startswith("TEMP_")] if zone.GetIsRuleArea() and zone.GetZoneName().startswith("TEMP_")]
for zone in doomed: for zone in doomed:
board.Remove(zone) remove(board, zone)
return len(doomed) return len(doomed)
@@ -119,11 +137,13 @@ def route_rf(board: pcbnew.BOARD) -> None:
w = 0.164 w = 0.164
j2 = pad_xy(board, "J2", "1") j2 = pad_xy(board, "J2", "1")
j2_inner = (j2[0] - 0.50, j2[1])
dc_block_1 = pad_xy(board, "C83", "1") dc_block_1 = pad_xy(board, "C83", "1")
l4_2 = pad_xy(board, "L4", "2") l4_2 = pad_xy(board, "L4", "2")
add_polyline(board, "RF_SMA", [j2, l4_2, dc_block_1], w) add_polyline(board, "RF_SMA", [j2_inner, (170.10, 42.09), l4_2, dc_block_1], w)
d1 = pad_xy(board, "D1", "1") # D1 sits directly on the 50-ohm line: its pad 1 overlaps the run above,
add_polyline(board, "RF_SMA", [d1, (d1[0], 45.07)], w) # so no spur is drawn. On a 1.34-mm stub the 0.3-pF clamp presented
# -j228 ohm at L1 and cost 3.4 dB of return loss for nothing.
add_polyline(board, "RF_PRE_SAW", add_polyline(board, "RF_PRE_SAW",
[pad_xy(board, "C83", "2"), pad_xy(board, "FL1", "1")], w) [pad_xy(board, "C83", "2"), pad_xy(board, "FL1", "1")], w)
@@ -146,12 +166,16 @@ def route_usb(board: pcbnew.BOARD) -> None:
# mirrored USB-C contacts join through a sub-1.5-mm L3 crossover so neither # mirrored USB-C contacts join through a sub-1.5-mm L3 crossover so neither
# polarity creates a same-layer crossing at the receptacle. # polarity creates a same-layer crossing at the receptacle.
w = 0.135 w = 0.135
dm_main = pad_xy(board, "J1", "B7") for ref, expected in {
dp_main = pad_xy(board, "J1", "A6") "R62": {"USB_D_P", "FTDI_D_P"},
dm_mirror = pad_xy(board, "J1", "A7") "R63": {"USB_D_N", "FTDI_D_N"},
dp_mirror = pad_xy(board, "J1", "B6") }.items():
dm_join, dp_join = (28.30, 47.25), (28.80, 47.75) assert_ref_nets(board, ref, expected)
dm_aux, dp_aux = (27.30, 48.25), (27.80, 48.75)
dm_main, dp_main = pad_xy(board, "J1", "B7"), pad_xy(board, "J1", "A6")
dm_mirror, dp_mirror = pad_xy(board, "J1", "A7"), pad_xy(board, "J1", "B6")
dm_join, dp_join = (28.60, 47.25), (29.20, 47.75)
dm_aux, dp_aux = (27.60, 48.25), (28.20, 48.75)
add_polyline(board, "USB_D_N", [dm_main, dm_join], w) add_polyline(board, "USB_D_N", [dm_main, dm_join], w)
add_polyline(board, "USB_D_P", [dp_main, dp_join], w) add_polyline(board, "USB_D_P", [dp_main, dp_join], w)
@@ -163,21 +187,33 @@ def route_usb(board: pcbnew.BOARD) -> None:
add_polyline(board, "USB_D_N", [dm_aux, dm_join], w, pcbnew.In2_Cu) add_polyline(board, "USB_D_N", [dm_aux, dm_join], w, pcbnew.In2_Cu)
add_polyline(board, "USB_D_P", [dp_aux, dp_join], w, pcbnew.In2_Cu) add_polyline(board, "USB_D_P", [dp_aux, dp_join], w, pcbnew.In2_Cu)
# Down the free channel east of J1 into the TPD2EUSB30A clamp pads. D_P
# drops at x=30.00 rather than on its own pad column so it keeps 0.2325 mm
# to the D_N clamp pad, then jogs 45 degrees onto U6.1.
dm_esd = pad_xy(board, "U6", "3") dm_esd = pad_xy(board, "U6", "3")
dp_esd = pad_xy(board, "U6", "1") dp_esd = pad_xy(board, "U6", "1")
add_polyline(board, "USB_D_N", [dm_join, (30.00, 49.00), (30.00, 50.70), dm_esd], w)
add_polyline(board, "USB_D_P", [dp_join, (30.285, 49.285), (30.285, 51.64), dp_esd], w)
dm_r = pad_xy(board, "R64", "1")
dp_r = pad_xy(board, "R63", "1")
add_polyline(board, "USB_D_N", add_polyline(board, "USB_D_N",
[dm_esd, (31.00, 51.40), (34.00, 54.40), (35.80, 54.40), dm_r], w) [dm_join, (29.60, 47.25), (30.50, 48.15), dm_esd], w)
add_polyline(board, "USB_D_P", add_polyline(board, "USB_D_P",
[dp_esd, (30.715, 51.685), (33.715, 54.685), (35.80, 54.685), dp_r], w) [dp_join, (29.45, 47.75), (30.00, 48.30),
(30.00, 51.775), dp_esd], w)
# Equal 1.511-mm post-termination legs into the FT4232H USB PHY. # Both clamp taps run as a 0.5-mm-pitch 45-degree pair to the series
add_polyline(board, "FTDI_D_N", [pad_xy(board, "R64", "2"), pad_xy(board, "U2", "7")], w) # terminators. D_N leaves U6.3 straight east; D_P has to step south first,
add_polyline(board, "FTDI_D_P", [pad_xy(board, "R63", "2"), pad_xy(board, "U2", "8")], w) # because U6.2 (ground) sits directly east of U6.1 on the same pad row.
dm_r = pad_xy(board, "R63", "1")
dp_r = pad_xy(board, "R62", "1")
add_polyline(board, "USB_D_N",
[dm_esd, (31.000, 51.075), (34.425, 54.50), dm_r], w)
add_polyline(board, "USB_D_P",
[dp_esd, (30.150, 52.500), (31.718, 52.500),
(34.718, 55.50), dp_r], w)
# Equal short post-termination legs into the FT4232H USB PHY.
add_polyline(board, "FTDI_D_N",
[pad_xy(board, "R63", "2"), (37.76, 54.75), pad_xy(board, "U2", "7")], w)
add_polyline(board, "FTDI_D_P",
[pad_xy(board, "R62", "2"), (37.76, 55.25), pad_xy(board, "U2", "8")], w)
def add_rf_ground(board: pcbnew.BOARD) -> None: def add_rf_ground(board: pcbnew.BOARD) -> None:
@@ -195,14 +231,246 @@ def add_rf_ground(board: pcbnew.BOARD) -> None:
outline.Append(point(x, y)) outline.Append(point(x, y))
board.Add(zone) board.Add(zone)
# J2 uses separate top- and bottom-side SMT lands with the same pad number.
# The top lands overlap the RF ground zone, but a bottom land whose centre
# is outside the outline cannot acquire a thermal from the board-wide B.Cu
# pour. Start inside each 1.5-mm-wide land and stop far enough inboard for
# the existing B.Cu pour to absorb the bond. At x=174.50 a 0.40-mm track
# retains 0.30 mm to the x=175.00 routed edge (rule minimum 0.25 mm).
for y in (37.75, 46.25):
add_segment(board, "GND", (174.50, y), (173.50, y),
0.40, pcbnew.B_Cu)
# Lambda/20 is about 9.5 mm at GPS L1 in FR-4. This 2-mm fence is much # Lambda/20 is about 9.5 mm at GPS L1 in FR-4. This 2-mm fence is much
# tighter and also gives every local shunt/EP a short plane connection. # tighter and also gives every local shunt/EP a short plane connection.
fence = [(x, 38.7) for x in range(153, 174, 2)] fence = [(x, 38.7) for x in range(153, 174, 2)]
fence += [(x, 45.0) for x in range(161, 174, 2)] fence += [(x, 45.0) for x in range(161, 174, 2)]
existing_vias = [item for item in board.GetTracks()
if isinstance(item, pcbnew.PCB_VIA)]
# The fence goes in after the session has been imported, so by now there is
# routed copper on F.Cu and B.Cu inside this region as well. Checking only
# vias and pads let a fence via land straight on top of a signal trace.
other_tracks = [item for item in board.GetTracks()
if not isinstance(item, pcbnew.PCB_VIA)
and item.GetNetname() != "GND"]
all_pads = [pad for fp in board.GetFootprints() for pad in fp.Pads()]
via_radius = 0.225 + 0.10
for location in fence: for location in fence:
candidate = point(*location)
blocked = any(
item.GetNetname() != "GND" and
math.hypot(pcbnew.ToMM(item.GetPosition().x - candidate.x),
pcbnew.ToMM(item.GetPosition().y - candidate.y)) < 0.75
for item in existing_vias
)
if not blocked:
x, y = location
for pad in all_pads:
if pad.GetNetname() == "GND":
continue
box = pad.GetBoundingBox()
if (pcbnew.ToMM(box.GetX()) - 0.35 <= x <= pcbnew.ToMM(box.GetRight()) + 0.35 and
pcbnew.ToMM(box.GetY()) - 0.35 <= y <= pcbnew.ToMM(box.GetBottom()) + 0.35):
blocked = True
break
if not blocked:
for item in other_tracks:
start, end = item.GetStart(), item.GetEnd()
reach = via_radius + pcbnew.ToMM(item.GetWidth()) / 2
if route_local_power.segment_distance(location,
(pcbnew.ToMM(start.x), pcbnew.ToMM(start.y)),
(pcbnew.ToMM(end.x), pcbnew.ToMM(end.y))) < reach:
blocked = True
break
if not blocked:
add_via(board, "GND", location, 0.45, 0.225) add_via(board, "GND", location, 0.45, 0.225)
NETCLASS_WIDTH = {"Default": 0.12, "BGA_ESCAPE": 0.10, "CLOCK": 0.15,
"POWER": 0.40}
MIN_TRACK_WIDTH = 0.10
MIN_CLEARANCE = 0.10
# These local rails carry bias or IC-internal supply current and may safely use
# the finisher's 0.25-mm POWER width. The 5-V input paths and FPGA rails are
# deliberately absent: they must be completed by the plane/local-power stage
# at their full width, never "made connected" with a signal-sized neck.
FINE_POWER_NETS = {
"FTDI_1V8", "FTDI_VPHY", "FTDI_VPLL",
"IMU_3V3", "IMU_VREGA2", "IMU_VREGD",
"ANT_BIAS_SEL", "ANT_BIAS_SW",
}
def normalize_imported_widths(board: pcbnew.BOARD) -> tuple[int, int]:
"""Restore the net class width on copper the session came back too thin.
Freerouting's auto-routing stage honours the DSN rule width exactly, but
its fanout stage emits three quarters of it, which lands below this board's
0.10-mm minimum track width. The affected pieces are the short stubs at
the pads, and widening one can walk it into a neighbouring pad, so each is
grown only as far as it still clears everything else - the class width
where there is room, the board minimum where there is not.
"""
obstacles = route_local_power.Obstacles(board)
doomed = []
widened = 0
for item in board.GetTracks():
if isinstance(item, pcbnew.PCB_VIA) or item.IsLocked():
continue
target = NETCLASS_WIDTH.get(item.GetNetClassName())
if target is None or pcbnew.ToMM(item.GetWidth()) >= target - 1e-6:
continue
start = (pcbnew.ToMM(item.GetStart().x), pcbnew.ToMM(item.GetStart().y))
end = (pcbnew.ToMM(item.GetEnd().x), pcbnew.ToMM(item.GetEnd().y))
for width in (target, MIN_TRACK_WIDTH):
# Judge existing copper against the rule itself, not the extra
# margin the local router leaves itself when placing new copper.
if obstacles.track_clear(item.GetNetname(), start, end,
width, item.GetLayer(),
clearance_floor=MIN_CLEARANCE):
item.SetWidth(mm(width))
widened += 1
break
else:
# Nothing legal fits here, so this piece cannot ship. Dropping it
# unravels its net back to the pads, and the completion pass
# further down rebuilds the connection at the proper width.
doomed.append(item)
for item in doomed:
remove(board, item)
return widened, len(doomed)
def remove_dangling_copper(board: pcbnew.BOARD) -> tuple[int, int]:
"""Delete copper the finished routing left hanging.
A ball whose net ended up routed entirely on F.Cu leaves its pre-routed
dogbone via connected on one layer only, and removing that via in turn
strands the stub that fed it. Both are drilled holes and copper with no
purpose, so this repeats until nothing new comes loose.
"""
vias = tracks = 0
for _round in range(8):
board.BuildConnectivity()
connectivity = board.GetConnectivity()
doomed = [item for item in board.GetTracks()
if item.GetNetname() not in CRITICAL_NETS
and connectivity.TestTrackEndpointDangling(item, False)]
if not doomed:
break
for item in doomed:
if isinstance(item, pcbnew.PCB_VIA):
vias += 1
else:
tracks += 1
remove(board, item)
board.BuildConnectivity()
return vias, tracks
def drop_plane_islands(board: pcbnew.BOARD) -> int:
"""Discard unconnected plane fragments left by the escape via anti-pads.
The generated planes keep every fragment while the board is unrouted so the
Specctra export still shows the router a plane target under each via field.
A finished board must not ship floating copper, so the final fill removes
any island the completed routing did not connect.
"""
changed = 0
for zone in board.Zones():
if zone.GetIsRuleArea() or zone.GetNetname() == "":
continue
zone.SetIslandRemovalMode(pcbnew.ISLAND_REMOVAL_MODE_ALWAYS)
changed += 1
return changed
def add_named_rule_areas(board: pcbnew.BOARD) -> None:
"""Permanent named areas that only exist so custom DRC rules can name them.
The TPD2EUSB30A is a SOT-23 clamp whose ground pad sits between its two
channel pads, so USB_D_P and USB_D_N must separate for about 1.5 mm to pass
it. That is normal for this package, but it does break the 0.90-mm coupled
gap the pair holds everywhere else, so the rule is scoped to skip this one
breakout instead of being relaxed board-wide.
RF_NO_DIGITAL names the region under the GPS front end. The TEMP_ version
of it exists only while the autorouter runs, so once that is deleted
nothing would stop a later hand edit from dropping a digital trace onto L3,
L5, L6 or B.Cu directly under the RF line. A plain keepout cannot be used
here because it would also reject the ground stitching vias and the
MAX2771's own supply drops, so the area carries no constraint itself and
board.kicad_dru scopes the disallow to non-power net classes. It
deliberately stops short of L2 and L7: those must stay solid and unbroken
under the RF path, which is exactly what an unrouted plane layer gives.
"""
below = pcbnew.LSET()
for layer in (pcbnew.In2_Cu, pcbnew.In3_Cu, pcbnew.In4_Cu, pcbnew.In5_Cu,
pcbnew.B_Cu):
below.AddLayer(layer)
top = pcbnew.LSET()
top.AddLayer(pcbnew.F_Cu)
areas = (
("USB_ESD_BREAKOUT", (29.90, 50.70, 32.10, 53.10), top),
("RF_NO_DIGITAL", (152.00, 36.00, 174.50, 43.70), below),
)
for name, bounds, layers in areas:
x0, y0, x1, y1 = bounds
area = pcbnew.ZONE(board)
area.SetZoneName(name)
area.SetIsRuleArea(True)
area.SetLayerSet(layers)
area.SetDoNotAllowTracks(False)
area.SetDoNotAllowVias(False)
area.SetDoNotAllowPads(False)
area.SetDoNotAllowFootprints(False)
area.SetDoNotAllowZoneFills(False)
outline = area.Outline()
outline.NewOutline()
for x, y in ((x0, y0), (x1, y0), (x1, y1), (x0, y1)):
outline.Append(point(x, y))
board.Add(area)
def add_rf_launch_relief(board: pcbnew.BOARD) -> None:
"""Void the reference planes under the SMA edge-launch pad.
J2's centre land is 1.5 mm wide because that is what the connector's
centre pin needs mechanically, but 1.5 mm of copper 0.0994 mm above a
solid L2_GND is a 10.7-ohm transmission line, not a 50-ohm one. The
1.84 mm of it that sits over the plane is 6.7 degrees long at L1 and
reflects 12 dB - by far the worst discontinuity in the RF path, and worse
than everything the 0.164-mm line downstream is carefully built to avoid.
Removing the plane under the land moves its reference down to L4_PWR
(VCC3V0_RF, an AC ground) 0.587 mm below, which raises the launch to about
37 ohms and 30 dB. L3_SIG carries no pour and no copper here, so this
only has to void L2; the area covers L3 as well so a later edit cannot
fill the gap back in. It stops at x = 172.35, where the land ends and the
0.164-mm line begins, so the line keeps its own L2 reference.
"""
relief = pcbnew.LSET()
for layer in (pcbnew.In1_Cu, pcbnew.In2_Cu): # L2_GND, L3_SIG
relief.AddLayer(layer)
area = pcbnew.ZONE(board)
area.SetZoneName("RF_LAUNCH_RELIEF")
area.SetIsRuleArea(True)
area.SetLayerSet(relief)
area.SetDoNotAllowTracks(True)
area.SetDoNotAllowVias(True)
area.SetDoNotAllowZoneFills(True)
area.SetDoNotAllowPads(False)
area.SetDoNotAllowFootprints(False)
outline = area.Outline()
outline.NewOutline()
for x, y in ((172.35, 40.95), (175.10, 40.95), (175.10, 43.05), (172.35, 43.05)):
outline.Append(point(x, y))
board.Add(area)
def routed_lengths(board: pcbnew.BOARD) -> dict[str, float]: def routed_lengths(board: pcbnew.BOARD) -> dict[str, float]:
totals = {net: 0.0 for net in CRITICAL_NETS} totals = {net: 0.0 for net in CRITICAL_NETS}
for item in board.GetTracks(): for item in board.GetTracks():
@@ -217,26 +485,91 @@ def routed_lengths(board: pcbnew.BOARD) -> dict[str, float]:
def main() -> None: def main() -> None:
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("session", type=Path, help="Freerouting .ses file") parser.add_argument("session", type=Path, help="Freerouting .ses file")
parser.add_argument("--base", type=Path, default=POWER_LOCAL,
help="Board used to create the routed DSN")
parser.add_argument("--output", type=Path, default=ROUTED) parser.add_argument("--output", type=Path, default=ROUTED)
parser.add_argument(
"--finish-nets", default="",
help="Comma- or semicolon-separated residual nets to close with the "
"fine local maze before dangling-copper cleanup; use 'auto' to "
"consume the local router's exact unresolved-net result",
)
args = parser.parse_args() args = parser.parse_args()
board = pcbnew.LoadBoard(str(PLACED)) board = pcbnew.LoadBoard(str(args.base))
if not pcbnew.ImportSpecctraSES(board, str(args.session)): if not pcbnew.ImportSpecctraSES(board, str(args.session)):
raise RuntimeError(f"Could not import {args.session}") raise RuntimeError(f"Could not import {args.session}")
removed = remove_critical_routes(board) removed = remove_critical_routes(board)
widened, dropped = normalize_imported_widths(board)
# Keep imported dogbones and dangling track ends until after the focused
# finisher. They are useful legal anchors for the few connections the
# global router leaves open. The old ordering deleted those anchors first
# and then forced every residual route to find a brand-new via beside a
# fine-pitch pad, which is exactly where there is no room.
stranded_vias = stranded_tracks = 0
removed_keepouts = remove_temporary_keepouts(board) removed_keepouts = remove_temporary_keepouts(board)
route_rf(board) route_rf(board)
route_usb(board) route_usb(board)
add_rf_ground(board) add_rf_ground(board)
add_rf_launch_relief(board)
add_named_rule_areas(board)
# The reserved RF and USB corridors are gone now, so the power pads that
# sit inside them can finally reach their planes. Both passes are the same
# deterministic code used earlier in the pipeline.
if not pcbnew.ZONE_FILLER(board).Fill(board.Zones()):
raise RuntimeError("Zone fill before final power completion failed")
stitched, _unreachable = stitch_plane_pads.stitch(board)
if not pcbnew.ZONE_FILLER(board).Fill(board.Zones()):
raise RuntimeError("Zone fill after final plane stitching failed")
remaining = route_local_power.complete(board, signals=True)
requested = [name.strip()
for name in args.finish_nets.replace(";", ",").split(",")
if name.strip()]
if requested == ["auto"]:
candidates = []
class_priority = {"CLOCK": 0, "BGA_ESCAPE": 1, "Default": 2}
for name in remaining:
net = board.FindNet(name)
if net is None:
continue
klass = net.GetNetClassName()
if klass in {"Default", "BGA_ESCAPE", "CLOCK"} or name in FINE_POWER_NETS:
# Route the widest/most constrained clocks before ordinary
# signals can spend their channels; local low-current rails
# are last because their endpoints are confined to one IC.
priority = class_priority.get(klass, 3)
candidates.append((priority, name))
finish_nets = [name for _priority, name in sorted(candidates)]
print("Fine-maze residuals:", ", ".join(finish_nets) or "none")
else:
finish_nets = requested
if finish_nets:
board.BuildConnectivity()
maze = finish_signal_routes.FineMaze(board)
for net_name in finish_nets:
if board.FindNet(net_name) is None:
raise RuntimeError(f"Unknown --finish-nets entry: {net_name}")
finish_signal_routes.finish_net(maze, net_name)
late_vias, late_tracks = remove_dangling_copper(board)
cleaned = drop_plane_islands(board)
if not pcbnew.ZONE_FILLER(board).Fill(board.Zones()): if not pcbnew.ZONE_FILLER(board).Fill(board.Zones()):
raise RuntimeError("Final zone fill failed") raise RuntimeError("Final zone fill failed")
pcbnew.SaveBoard(str(args.output), board) pcbnew.SaveBoard(str(args.output), board)
for suffix in (".kicad_pro", ".kicad_dru"): for suffix in (".kicad_pro", ".kicad_dru"):
source = PLACED.with_suffix(suffix) source = args.base.with_suffix(suffix)
if source.exists(): if source.exists():
shutil.copyfile(source, args.output.with_suffix(suffix)) shutil.copyfile(source, args.output.with_suffix(suffix))
print(f"Removed {removed} autorouted critical-net items") print(f"Removed {removed} autorouted critical-net items")
print(f"Widened {widened} imported tracks; dropped {dropped} that had no room")
print(f"Removed {stranded_vias + late_vias} unused vias and "
f"{stranded_tracks + late_tracks} stranded stubs")
print(f"Removed {removed_keepouts} temporary critical-route keepouts") print(f"Removed {removed_keepouts} temporary critical-route keepouts")
print("Late plane stitching:",
", ".join(f"{n}={k}" for n, k in sorted(stitched.items())) or "none")
print(f"Enabled island removal on {cleaned} plane zones")
print(f"Saved {args.output}") print(f"Saved {args.output}")
for net, length in sorted(routed_lengths(board).items()): for net, length in sorted(routed_lengths(board).items()):
print(f"{net:16s} {length:8.3f} mm") print(f"{net:16s} {length:8.3f} mm")
+403
View File
@@ -0,0 +1,403 @@
#!/usr/bin/env python3
"""Close a small, explicit set of residual PCB connections.
This is deliberately a *finisher*, not a full autorouter. It operates on the
real KiCad board after the global Specctra route has been imported. For each
requested net it finds the disconnected copper groups, chooses anchors from
pads, existing track endpoints and vias, and runs a fine 3-D A* search through
the board's actual copper obstacle model. Every committed segment and via is
locked; KiCad DRC remains the acceptance test.
Unlike the older residual helpers, an endpoint does not need room for a new
via. A route may leave a fine-pitch pad on its own copper layer, travel to a
legal via site, and only then change layers. Existing same-net vias and track
ends are also valid start/goal anchors.
"""
from __future__ import annotations
import argparse
import heapq
import math
import shutil
import time
from dataclasses import dataclass
from pathlib import Path
import pcbnew
import route_local_power as rlp
# A 0.20-mm grid lands on both halves of the Ti375's 0.8-mm dogbone lattice
# while keeping the Python obstacle search tractable. Endpoint stubs retain
# their exact coordinates, so off-grid package pads are not displaced.
STEP = 0.20
VIA_DIAMETER = 0.45
VIA_DRILL = 0.225
CLASS_RULES = {
"Default": (0.12, 0.10),
"BGA_ESCAPE": (0.10, 0.10),
"CLOCK": (0.15, 0.15),
# Only low-current local rails may be passed to this finisher. Main 5-V
# and converter paths need deterministic wide copper instead.
"POWER": (0.25, 0.10),
}
def mm(value: int) -> float:
return round(pcbnew.ToMM(value), 4)
def MM(value: float) -> int:
return pcbnew.FromMM(value)
@dataclass(frozen=True)
class Anchor:
point: tuple[float, float]
layers: tuple[int, ...]
label: str
class FineMaze:
def __init__(self, board: pcbnew.BOARD) -> None:
self.board = board
self.layers = tuple(
layer for layer in board.GetEnabledLayers().CuStack()
if board.GetLayerType(layer) == pcbnew.LT_SIGNAL
)
self.layer_rank = {layer: index for index, layer in enumerate(self.layers)}
self.obstacles = rlp.Obstacles(board)
def track_clear(self, net: str, a: tuple[float, float], b: tuple[float, float],
layer: int, width: float, clearance: float) -> bool:
return self.obstacles.track_clear(
net, a, b, width, layer, clearance_floor=clearance
)
def via_clear(self, net: str, p: tuple[float, float], clearance: float) -> bool:
return self.obstacles.via_clear(net, p, clearance_floor=clearance)
@staticmethod
def _collapse(points: list[tuple[float, float, int]]) -> list[tuple[float, float, int]]:
if len(points) < 3:
return points
out = [points[0]]
for index in range(1, len(points) - 1):
a = out[-1]
b = points[index]
c = points[index + 1]
if a[2] == b[2] == c[2]:
cross = (b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0])
if abs(cross) < 1e-8:
continue
out.append(b)
out.append(points[-1])
return out
def search(self, net: str, start: Anchor, goal: Anchor, width: float,
clearance: float, margin: float, deadline_s: float = 90.0,
node_cap: int = 700_000) -> list[tuple[float, float, int]] | None:
sx, sy = start.point
gx, gy = goal.point
x0 = min(sx, gx) - margin
y0 = min(sy, gy) - margin
x1 = max(sx, gx) + margin
y1 = max(sy, gy) + margin
def snap(value: float) -> int:
return round(value / STEP)
ix0, iy0 = snap(x0), snap(y0)
ix1, iy1 = snap(x1), snap(y1)
def point(ix: int, iy: int) -> tuple[float, float]:
return round(ix * STEP, 4), round(iy * STEP, 4)
def heuristic(ix: int, iy: int) -> float:
px, py = point(ix, iy)
return math.hypot(px - gx, py - gy)
start_layers = tuple(layer for layer in start.layers if layer in self.layer_rank)
goal_layers = frozenset(layer for layer in goal.layers if layer in self.layer_rank)
if not start_layers or not goal_layers:
return None
queue: list[tuple[float, float, tuple[int, int, int]]] = []
cost: dict[tuple[int, int, int], float] = {}
parent: dict[tuple[int, int, int], tuple[int, int, int] | None] = {}
# Seed several nearby grid points. The exact pad/via/track endpoint to
# grid stub is checked just like every other segment; no unchecked snap
# segment can leak into the output.
base_x, base_y = snap(sx), snap(sy)
for layer in start_layers:
for dx in (-1, 0, 1):
for dy in (-1, 0, 1):
ix, iy = base_x + dx, base_y + dy
if not (ix0 <= ix <= ix1 and iy0 <= iy <= iy1):
continue
p = point(ix, iy)
if p != start.point and not self.track_clear(
net, start.point, p, layer, width, clearance):
continue
state = (ix, iy, layer)
initial = math.hypot(p[0] - sx, p[1] - sy)
if initial < cost.get(state, math.inf):
cost[state] = initial
parent[state] = None
heapq.heappush(queue, (initial + heuristic(ix, iy), initial, state))
if not queue:
return None
stop = time.monotonic() + deadline_s
hit: tuple[int, int, int] | None = None
visited = 0
directions = ((-1, -1), (-1, 0), (-1, 1), (0, -1),
(0, 1), (1, -1), (1, 0), (1, 1))
while queue and visited < node_cap and time.monotonic() < stop:
_score, current_cost, state = heapq.heappop(queue)
if current_cost > cost.get(state, math.inf) + 1e-9:
continue
visited += 1
ix, iy, layer = state
here = point(ix, iy)
if layer in goal_layers and math.hypot(here[0] - gx, here[1] - gy) <= 0.30:
if here == goal.point or self.track_clear(
net, here, goal.point, layer, width, clearance):
hit = state
break
for dx, dy in directions:
nx, ny = ix + dx, iy + dy
if not (ix0 <= nx <= ix1 and iy0 <= ny <= iy1):
continue
nxt_point = point(nx, ny)
if not self.track_clear(net, here, nxt_point, layer, width, clearance):
continue
nxt = (nx, ny, layer)
move = STEP * (math.sqrt(2.0) if dx and dy else 1.0)
# Prefer inner signal layers slightly, but do not ban F/B.
outer_penalty = 0.015 if layer in (pcbnew.F_Cu, pcbnew.B_Cu) else 0.0
new_cost = current_cost + move + outer_penalty
if new_cost + 1e-9 < cost.get(nxt, math.inf):
cost[nxt] = new_cost
parent[nxt] = state
heapq.heappush(queue, (new_cost + heuristic(nx, ny), new_cost, nxt))
if self.via_clear(net, here, clearance):
for next_layer in self.layers:
if next_layer == layer:
continue
nxt = (ix, iy, next_layer)
new_cost = current_cost + 2.0
if new_cost + 1e-9 < cost.get(nxt, math.inf):
cost[nxt] = new_cost
parent[nxt] = state
heapq.heappush(queue, (new_cost + heuristic(ix, iy), new_cost, nxt))
if hit is None:
return None
states = []
cursor: tuple[int, int, int] | None = hit
while cursor is not None:
states.append(cursor)
cursor = parent[cursor]
states.reverse()
result = [(sx, sy, states[0][2])]
result.extend((*point(ix, iy), layer) for ix, iy, layer in states)
result.append((gx, gy, states[-1][2]))
return self._collapse(result)
def _same_net_via_at(self, net: str, p: tuple[float, float]) -> bool:
for item in self.board.GetTracks():
if not isinstance(item, pcbnew.PCB_VIA) or item.GetNetname() != net:
continue
q = item.GetPosition()
if math.hypot(mm(q.x) - p[0], mm(q.y) - p[1]) < 0.02:
return True
return False
def add_track(self, net: str, a: tuple[float, float], b: tuple[float, float],
layer: int, width: float) -> None:
if math.hypot(a[0] - b[0], a[1] - b[1]) < 1e-6:
return
track = pcbnew.PCB_TRACK(self.board)
track.SetStart(pcbnew.VECTOR2I_MM(*a))
track.SetEnd(pcbnew.VECTOR2I_MM(*b))
track.SetWidth(MM(width))
track.SetLayer(layer)
track.SetNet(self.board.FindNet(net))
track.SetLocked(True)
self.board.Add(track)
track.thisown = False
self.obstacles.add_track(net, a, b, width, layer)
def add_via(self, net: str, p: tuple[float, float]) -> None:
if self._same_net_via_at(net, p):
return
via = pcbnew.PCB_VIA(self.board)
via.SetPosition(pcbnew.VECTOR2I_MM(*p))
via.SetWidth(MM(VIA_DIAMETER))
via.SetDrill(MM(VIA_DRILL))
via.SetLayerPair(pcbnew.F_Cu, pcbnew.B_Cu)
via.SetNet(self.board.FindNet(net))
via.SetLocked(True)
self.board.Add(via)
via.thisown = False
self.obstacles.add_via(net, p, VIA_DIAMETER / 2, VIA_DRILL / 2)
def commit(self, net: str, path: list[tuple[float, float, int]], width: float) -> None:
for a, b in zip(path, path[1:]):
if a[2] == b[2]:
self.add_track(net, a[:2], b[:2], a[2], width)
else:
if math.hypot(a[0] - b[0], a[1] - b[1]) > 0.02:
raise RuntimeError(f"Non-vertical layer change in {net}: {a} -> {b}")
self.add_via(net, a[:2])
def item_anchor(item, routing_layers: tuple[int, ...], label: str) -> list[Anchor]:
if isinstance(item, pcbnew.PAD):
p = item.GetPosition()
layers = tuple(layer for layer in item.GetLayerSet().Seq() if layer in routing_layers)
return [Anchor((mm(p.x), mm(p.y)), layers, label)] if layers else []
if isinstance(item, pcbnew.PCB_VIA):
p = item.GetPosition()
return [Anchor((mm(p.x), mm(p.y)), routing_layers, label)]
if isinstance(item, pcbnew.PCB_TRACK):
layer = item.GetLayer()
if layer not in routing_layers:
return []
a, b = item.GetStart(), item.GetEnd()
return [Anchor((mm(a.x), mm(a.y)), (layer,), label + ":start"),
Anchor((mm(b.x), mm(b.y)), (layer,), label + ":end")]
return []
def copper_groups(board: pcbnew.BOARD, net: str,
routing_layers: tuple[int, ...]) -> list[tuple[list[pcbnew.PAD], list[Anchor]]]:
connectivity = board.GetConnectivity()
pads = [pad for fp in board.GetFootprints() for pad in fp.Pads()
if pad.GetNetname() == net]
seen: set[str] = set()
groups = []
for pad in pads:
uid = pad.m_Uuid.AsString()
if uid in seen:
continue
connected = list(connectivity.GetConnectedItems(pad, 0))
if all(item.m_Uuid.AsString() != uid for item in connected):
connected.append(pad)
group_pads = [item for item in connected if isinstance(item, pcbnew.PAD)]
if not group_pads:
group_pads = [pad]
seen.update(item.m_Uuid.AsString() for item in group_pads)
anchors: list[Anchor] = []
anchor_seen = set()
for index, item in enumerate(connected):
for anchor in item_anchor(item, routing_layers, f"{item.GetClass()}#{index}"):
key = (anchor.point, anchor.layers)
if key not in anchor_seen:
anchors.append(anchor)
anchor_seen.add(key)
if not anchors:
anchors.extend(item_anchor(pad, routing_layers, "pad"))
groups.append((group_pads, anchors))
return groups
def class_rule(board: pcbnew.BOARD, net: str) -> tuple[float, float, str]:
for fp in board.GetFootprints():
for pad in fp.Pads():
if pad.GetNetname() == net:
name = pad.GetNetClassName()
width, clearance = CLASS_RULES.get(name, CLASS_RULES["Default"])
return width, clearance, name
raise RuntimeError(f"Net {net} has no pads")
def finish_net(maze: FineMaze, net: str) -> int:
board = maze.board
joined = 0
while True:
board.BuildConnectivity()
groups = copper_groups(board, net, maze.layers)
if len(groups) < 2:
return joined
groups.sort(key=lambda group: len(group[0]), reverse=True)
main = groups[0]
branch = min(groups[1:], key=lambda group: len(group[0]))
width, clearance, class_name = class_rule(board, net)
pairs = []
for source in branch[1]:
for target in main[1]:
distance = math.hypot(source.point[0] - target.point[0],
source.point[1] - target.point[1])
pairs.append((distance, source, target))
pairs.sort(key=lambda entry: entry[0])
routed = False
for distance, source, target in pairs[:10]:
for margin in (5.0, 12.0):
path = maze.search(net, source, target, width, clearance, margin,
deadline_s=20.0, node_cap=240_000)
if path is None:
continue
maze.commit(net, path, width)
board.BuildConnectivity()
print(f"{net}: joined {source.label} -> {target.label}; "
f"class={class_name} width={width:.3f} mm span={distance:.1f} mm")
joined += 1
routed = True
break
if routed:
break
if not routed:
print(f"{net}: FAILED ({len(groups)} copper groups, {len(pairs)} anchor pairs)")
return joined
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("board", type=Path)
parser.add_argument("--output", type=Path)
parser.add_argument("--nets", required=True,
help="Comma- or semicolon-separated residual net names")
args = parser.parse_args()
output = args.output or args.board
nets = [name.strip() for name in args.nets.replace(";", ",").split(",")
if name.strip()]
board = pcbnew.LoadBoard(str(args.board))
if not pcbnew.ZONE_FILLER(board).Fill(board.Zones()):
raise RuntimeError("Zone refill failed before residual routing")
board.BuildConnectivity()
maze = FineMaze(board)
for net in nets:
if board.FindNet(net) is None:
raise RuntimeError(f"Unknown net: {net}")
finish_net(maze, net)
if not pcbnew.ZONE_FILLER(board).Fill(board.Zones()):
raise RuntimeError("Zone refill failed after residual routing")
board.BuildConnectivity()
pcbnew.SaveBoard(str(output), board)
if output != args.board:
for suffix in (".kicad_pro", ".kicad_dru"):
source = args.board.with_suffix(suffix)
if source.exists():
shutil.copyfile(source, output.with_suffix(suffix))
print(f"Unconnected endpoints: {board.GetConnectivity().GetUnconnectedCount(True)}")
print(f"Saved {output}")
if __name__ == "__main__":
main()
+328 -90
View File
@@ -29,6 +29,13 @@ STOCK = Path("/usr/share/kicad/footprints")
# field allow a compact 155 x 90 mm outline. # field allow a compact 155 x 90 mm outline.
X0, Y0, X1, Y1 = 20.0, 20.0, 175.0, 110.0 X0, Y0, X1, Y1 = 20.0, 20.0, 175.0, 110.0
# Anti-pad clearance for planes that have to stay continuous under the Ti375's
# 0.8-mm escape via grid. This is the board's declared minimum copper
# clearance; anything larger merges neighbouring anti-pads and erases the
# plane across the whole BGA footprint. Split L4 islands keep the wider
# default so the gaps between different DC rails stay generous.
PLANE_CLEARANCE = 0.10
def mm(value: float) -> int: def mm(value: float) -> int:
return pcbnew.FromMM(value) return pcbnew.FromMM(value)
@@ -55,8 +62,15 @@ MAJOR = {
# USB-C and debug block. # USB-C and debug block.
"J1": (23.1, 48, 270), "U5": (30.5, 44.5, 0), "U6": (30.5, 51.5, 0), "J1": (23.1, 48, 270), "U5": (30.5, 44.5, 0), "U6": (30.5, 51.5, 0),
"U2": (43, 55, 0), "U25": (35, 62, 0), "U3": (51, 63, 0), "U2": (43, 55, 0), "U25": (35, 62, 0), "U3": (51, 65, 0),
"U29": (60, 54.5, 0), "U30": (67, 58.5, 0), "U31": (62, 63.5, 0), # The three FTDI-to-FPGA level buffers pulled west toward the FT4232H so
# BDBUS/CDBUS source nets reach them without threading the whole cluster.
"U29": (52.5, 54, 0), "U30": (60, 58, 0), "U31": (55.5, 62.5, 0),
# Dedicated USB-C power input, stacked in the gap between mounting hole H1
# and the J1 debug block on the same west edge. U35 sits at the same
# relative offset from J7 that U5 sits from J1.
"J7": (23.1, 35, 270), "U35": (30.5, 31.5, 0),
# Mechanically quiet IMU region. # Mechanically quiet IMU region.
"U22": (51, 84, 0), "U22": (51, 84, 0),
@@ -78,7 +92,7 @@ MAJOR = {
"U33": (87, 103, 0), "U33": (87, 103, 0),
# RF front end. J2's launch reference is exactly on the right PCB edge. # RF front end. J2's launch reference is exactly on the right PCB edge.
"J2": (175, 42, 0), "D1": (170.45, 40.75, 0), "FL1": (164.0, 42.1, 180), "J2": (175, 42, 0), "D1": (171.30, 41.715, 270), "FL1": (164.0, 42.1, 180),
"J5": (171, 56, 0), "U27": (165, 52, 0), "U21": (155.5, 45, 0), "J5": (171, 56, 0), "U27": (165, 52, 0), "U21": (155.5, 45, 0),
"U23": (149, 53, 0), "U24": (137, 55, 0), "U23": (149, 53, 0), "U24": (137, 55, 0),
} }
@@ -91,18 +105,61 @@ SPECIAL = {
# likewise land on the two controlled-impedance sections. # likewise land on the two controlled-impedance sections.
"C83": (167.5, 42.1, 180), "C84": (162.15, 41.45, 90), "R39": (160.5, 42.1, 180), "C83": (167.5, 42.1, 180), "C84": (162.15, 41.45, 90), "R39": (160.5, 42.1, 180),
"C85": (159.0, 40.70, 90), "L4": (169.5, 42.585, 90), "C85": (159.0, 40.70, 90), "L4": (169.5, 42.585, 90),
"L1": (105.0, 91.0, 0), "L2": (115.0, 91.0, 0), "L3": (130.0, 91.0, 0), # Converter magnetics and capacitors form compact current loops. U13's
"FB1": (60.0, 88.5, 0), "FB2": (36.0, 49.0, 0), "FB3": (39.0, 46.5, 0), # SW bank faces south, so L1 is vertical; U14/U15 SW pins face east and
"R63": (37.0, 55.50, 0), "R64": (37.0, 54.50, 0), # their inductors/output capacitors continue directly to the right.
"L1": (100.0, 98.5, 270), "L2": (118.0, 97.0, 0), "L3": (133.0, 97.0, 0),
"C18": (95.7, 91.8, 180), "C19": (95.7, 94.4, 180),
"C20": (104.2, 91.8, 0), "C21": (104.2, 94.4, 0),
"C22": (95.5, 103.5, 270), "C23": (99.0, 103.5, 270),
"C24": (102.5, 103.5, 270), "C25": (106.0, 103.5, 270),
"C26": (112.5, 96.2, 180), "C27": (112.5, 97.8, 180),
"C28": (121.3, 96.0, 0), "C29": (121.3, 98.3, 0),
"C30": (127.5, 96.2, 180), "C31": (127.5, 97.8, 180),
"C32": (136.3, 96.0, 0), "C33": (136.3, 98.3, 0),
# Core remote-sense and compensation stay on the quiet north side of U13.
"C16": (96.2, 89.0, 180), "R29": (98.1, 89.0, 180),
"C17": (100.0, 89.0, 0), "R27": (104.0, 87.8, 180),
"R28": (104.0, 89.2, 180),
"R31": (115.0, 94.3, 0), "R32": (130.0, 94.3, 0),
# FB3 feeds the FT4232H PLL supply on pin 9, which sits south of the USB
# pair corridor. Keeping the bead on that side avoids a supply trace that
# would otherwise have to cross the reserved differential run.
"FB1": (60.0, 88.5, 0), "FB2": (36.0, 49.0, 0), "FB3": (36.5, 58.2, 0),
# The 12-MHz series damping resistor moves west with its oscillator to keep
# the space south of the FT4232H free for FB3.
"R67": (32.0, 58.0, 0),
# USB series pair and FT4232 REF resistor follow the device pin order.
"R62": (37.0, 55.50, 0), "R63": (37.0, 54.50, 0),
# R64 is the FT4232H reference resistor, not part of the pair; it moves off
# the terminator column so the reserved pair corridor stays empty.
"R64": (36.0, 51.50, 0),
# The shield network belongs at the receptacle shell, not east of the
# contacts: automatic placement filled the only corridor that carries
# USB_D_P/USB_D_N away from J1. Pin all three parts south of the shell,
# clear of the pair's diagonal run towards the series terminators.
"C2": (24.0, 55.5, 0), "R9": (26.5, 57.5, 0), "R10": (29.5, 57.5, 0),
# MAX2771 output damping sits at the receiver; FPGA-driven 3-wire control # MAX2771 output damping sits at the receiver; FPGA-driven 3-wire control
# damping sits at the BGA source. R55 holds SHDN low at the receiver. # damping sits at the BGA source. R55 holds SHDN low at the receiver.
"R45": (151.0, 44.5, 0), "R45": (151.0, 44.5, 0),
"R46": (154.6, 50.2, 90), "R47": (155.8, 50.2, 90), # Widened from 1.2 mm to 2.0 mm pitch and dropped 1.3 mm south of the
"R48": (157.0, 50.2, 90), "R49": (158.2, 50.2, 90), # MAX2771 so each debug tap (MAX_I0_DEV etc.) has its own escape lane out
# from under U21.
"R46": (153.6, 51.5, 90), "R47": (155.6, 51.5, 90),
"R48": (157.6, 51.5, 90), "R49": (159.6, 51.5, 90),
"R50": (160.5, 47.5, 90), "R50": (160.5, 47.5, 90),
"R51": (121.5, 65.5, 0), "R52": (121.5, 66.7, 0), "R51": (121.5, 65.5, 0), "R52": (121.5, 66.7, 0),
"R53": (121.5, 67.9, 0), "R54": (121.5, 69.1, 0), "R53": (121.5, 67.9, 0), "R54": (121.5, 69.1, 0),
"R55": (161.7, 44.5, 90), "C89": (146.5, 53.0, 0), "R55": (161.7, 44.5, 90),
# PLL loop filter. CPOUT is a high-impedance charge-pump node, so the
# whole branch sits in the free pocket at U21's north-west corner rather
# than being spread around the package: pin 1 is 2 mm from the 15-pF shunt
# and the entire net stays on F.Cu. Scattering these parts is what forced
# CPOUT onto an inner layer underneath the GPS front end. The column stops
# short of x = 154.4 so it never enters the reserved RF corridor.
"C88": (153.60, 40.80, 90),
"R43": (153.60, 38.85, 90),
"C89": (154.80, 38.85, 90),
# 40-MHz termination is at U24. SafeSPI controller outputs are terminated # 40-MHz termination is at U24. SafeSPI controller outputs are terminated
# beside the FPGA; MISO and EXTRESN bias remain beside the IMU. # beside the FPGA; MISO and EXTRESN bias remain beside the IMU.
"R56": (140.2, 54.35, 0), "R56": (140.2, 54.35, 0),
@@ -120,7 +177,7 @@ SPECIAL = {
# connector-like test-point bank along the board edge. # connector-like test-point bank along the board edge.
SPECIAL.update({ SPECIAL.update({
"TP1": (37.5, 94.0, 0), "TP2": (45.5, 104.0, 0), "TP1": (37.5, 94.0, 0), "TP2": (45.5, 104.0, 0),
"TP3": (166.5, 54.0, 0), "TP4": (29.0, 39.0, 0), "TP3": (166.5, 54.0, 0), "TP4": (30.5, 36.5, 0),
"TP5": (22.0, 89.0, 0), "TP6": (49.5, 98.5, 0), "TP5": (22.0, 89.0, 0), "TP6": (49.5, 98.5, 0),
"TP7": (66.5, 90.0, 0), "TP8": (105.0, 98.0, 0), "TP7": (66.5, 90.0, 0), "TP8": (105.0, 98.0, 0),
"TP9": (118.0, 102.0, 0), "TP10": (133.0, 102.0, 0), "TP9": (118.0, 102.0, 0), "TP10": (133.0, 102.0, 0),
@@ -141,6 +198,8 @@ SPECIAL.update({
"C126": (136.0, 78.0, 0), "C127": (90.0, 100.5, 0), "C126": (136.0, 78.0, 0), "C127": (90.0, 100.5, 0),
"R104": (83.0, 86.8, 0), "R105": (83.0, 88.0, 0), "R104": (83.0, 86.8, 0), "R105": (83.0, 88.0, 0),
"R106": (83.0, 89.2, 0), "C128": (93.0, 88.0, 0), "R106": (83.0, 89.2, 0), "C128": (93.0, 88.0, 0),
# TPS2121 open-drain status pull-up, beside the U10 gate it holds up.
"R108": (53.0, 101.6, 0),
}) })
@@ -279,16 +338,29 @@ def configure_rules(board: pcbnew.BOARD) -> None:
def add_stackup(board: pcbnew.BOARD) -> None: def add_stackup(board: pcbnew.BOARD) -> None:
# Eight-layer 1.6 mm build, JLC08161H-3313. L2 and L7 are the two solid
# GND reference planes and carry no routing; L4 is the only dedicated power
# plane. That leaves five routable layers - F.Cu, L3, L5, L6 and B.Cu -
# which is what the Ti375 escape, the RF front end and the USB pair are
# built around. The two big FPGA I/O rails have no plane of their own and
# live as islands on L6 (VCC1V8) and B.Cu (VCC3V3). The outer F->L2 and
# L7->B prepreg is 3313 (0.0994 mm), which is what sets the F.Cu 50-ohm
# CPWG and 90-ohm USB geometries.
board.SetCopperLayerCount(8) board.SetCopperLayerCount(8)
board.SetLayerName(pcbnew.In1_Cu, "L2_GND") names = {
board.SetLayerName(pcbnew.In2_Cu, "L3_SIG") pcbnew.In1_Cu: "L2_GND", pcbnew.In2_Cu: "L3_SIG",
board.SetLayerName(pcbnew.In3_Cu, "L4_PWR") pcbnew.In3_Cu: "L4_PWR", pcbnew.In4_Cu: "L5_SIG",
board.SetLayerName(pcbnew.In4_Cu, "L5_SIG") pcbnew.In5_Cu: "L6_SIG", pcbnew.In6_Cu: "L7_GND",
board.SetLayerName(pcbnew.In5_Cu, "L6_SIG") }
board.SetLayerName(pcbnew.In6_Cu, "L7_GND") for layer, name in names.items():
board.SetLayerType(pcbnew.In1_Cu, pcbnew.LT_POWER) board.SetLayerName(layer, name)
board.SetLayerType(pcbnew.In3_Cu, pcbnew.LT_POWER) # L2/L7 solid GND, L4 the core + front-end power plane. The power plane
board.SetLayerType(pcbnew.In6_Cu, pcbnew.LT_POWER) # sits between GND and a routing layer and is not a layer the BGA escape
# needs, so all five routing layers (F, L3, L5, L6, B) stay usable.
for layer in (pcbnew.In1_Cu, pcbnew.In3_Cu, pcbnew.In6_Cu):
board.SetLayerType(layer, pcbnew.LT_POWER)
for layer in (pcbnew.In2_Cu, pcbnew.In4_Cu, pcbnew.In5_Cu):
board.SetLayerType(layer, pcbnew.LT_SIGNAL)
def place_components(board: pcbnew.BOARD) -> dict[str, pcbnew.FOOTPRINT]: def place_components(board: pcbnew.BOARD) -> dict[str, pcbnew.FOOTPRINT]:
@@ -420,12 +492,20 @@ def add_mounting_holes(board: pcbnew.BOARD) -> None:
def add_zone(board: pcbnew.BOARD, net_name: str, layer: int, def add_zone(board: pcbnew.BOARD, net_name: str, layer: int,
bounds: tuple[float, float, float, float], priority: int = 0) -> None: bounds: tuple[float, float, float, float], priority: int = 0,
clearance: float = 0.20) -> None:
"""Add a retained solid inner-layer plane region. """Add a retained solid inner-layer plane region.
These regions are retained even before fanout vias exist so the Specctra These regions are retained even before fanout vias exist so the Specctra
export contains real plane targets for the router. The final refill then export contains real plane targets for the router. The final refill then
clears anti-pads around every unrelated via and plated hole. clears anti-pads around every unrelated via and plated hole.
`clearance` decides whether a plane survives under the Ti375. Its escape
vias sit on a 0.8-mm grid, so at the default 0.20 mm every anti-pad
(0.225 + 0.20 = 0.425 mm radius) overlaps its neighbour and the plane is
erased across the whole BGA footprint. At the board minimum of 0.10 mm the
anti-pads are 0.325 mm and a 0.15-mm web survives between them, which is
what lets an isolated bank-supply ball reach its rail at all.
""" """
net = board.FindNet(net_name) net = board.FindNet(net_name)
if net is None: if net is None:
@@ -434,7 +514,7 @@ def add_zone(board: pcbnew.BOARD, net_name: str, layer: int,
zone = pcbnew.ZONE(board) zone = pcbnew.ZONE(board)
zone.SetLayer(layer) zone.SetLayer(layer)
zone.SetNet(net) zone.SetNet(net)
zone.SetLocalClearance(mm(0.20)) zone.SetLocalClearance(mm(clearance))
zone.SetMinThickness(mm(0.10)) zone.SetMinThickness(mm(0.10))
zone.SetThermalReliefGap(mm(0.20)) zone.SetThermalReliefGap(mm(0.20))
zone.SetThermalReliefSpokeWidth(mm(0.25)) zone.SetThermalReliefSpokeWidth(mm(0.25))
@@ -449,54 +529,116 @@ def add_zone(board: pcbnew.BOARD, net_name: str, layer: int,
def add_power_planes(board: pcbnew.BOARD) -> None: def add_power_planes(board: pcbnew.BOARD) -> None:
# Continuous reference planes immediately below the two outer signal """Eight-layer power: solid GND on L2 and L7, the core and front-end rails
# layers. A 0.50-mm inset exceeds JLCPCB's copper-to-edge requirement. on the L4 plane, and the two big FPGA I/O rails as islands on L6 (VCC1V8)
and B.Cu (VCC3V3).
There is no third plane layer to give the I/O rails, so they share the two
routing layers with the most free area. That is why those rails end up
fragmented by signal routing and why the residual-connection stage has to
treat a foreign pour as an obstacle.
"""
full = (X0 + 0.50, Y0 + 0.50, X1 - 0.50, Y1 - 0.50)
for layer in (pcbnew.In1_Cu, pcbnew.In6_Cu): for layer in (pcbnew.In1_Cu, pcbnew.In6_Cu):
add_zone(board, "GND", layer, (X0 + 0.50, Y0 + 0.50, X1 - 0.50, Y1 - 0.50)) add_zone(board, "GND", layer, full, clearance=PLANE_CLEARANCE)
# L4 is a deliberately split DC plane. Islands follow the functional # L4 (In3_Cu): core plus the 5-V input, the always-on auxiliary rail and
# floorplan; no high-speed route uses L4 and no signal crosses a split on # the four 3.0-V front-end rails. Islands follow the functional floorplan.
# either adjacent routing layer without a nearby return transition. l4 = (
islands = ( ("VCC3V3_AUX", (20.50, 36.00, 74.00, 72.00)),
("VCC3V3_AUX", (20.50, 38.00, 67.00, 72.00)), ("VCC3V3_AUX", (53.00, 88.00, 70.00, 105.00)),
("VCC3V3", (39.00, 73.00, 68.00, 85.10)), ("VCC3V3_AUX", (77.50, 84.00, 94.20, 100.50)),
("SYS5", (20.50, 85.50, 90.00, 109.50)), ("VCC3V3_AUX", (133.00, 74.50, 152.00, 82.00)),
("SYS5", (90.40, 106.40, 140.00, 109.50)), ("SYS5", (20.50, 85.50, 52.50, 109.50)),
# Core balls are interspersed with GND/auxiliary balls, so a complete ("SYS5", (89.50, 103.60, 145.00, 109.50)),
# local patch (with automatic anti-pads) replaces the invalid left/right ("SYS5", (140.00, 103.00, 174.50, 109.50)),
# voltage split that previously crossed the BGA field. ("SYS5", (144.00, 86.50, 174.50, 104.00)),
("VCC0V95", (95.00, 60.50, 119.00, 94.00)), ("SYS5", (157.00, 72.00, 174.50, 87.00)),
("VCC3V3", (119.50, 87.00, 140.00, 106.00)), ("SYS5", (157.00, 63.50, 174.50, 72.50)),
("VCC3V0_CLK", (126.00, 50.00, 148.50, 72.00)), ("VCC3V0_CLK", (126.00, 48.50, 156.00, 67.00)),
("VCC3V0_DIG", (119.50, 60.50, 125.50, 72.00)), ("VCC3V0_RF", (140.00, 58.50, 150.50, 65.50)),
("VCC3V0_DIG", (148.90, 50.40, 164.00, 72.00)), ("VCC3V0_DIG", (134.00, 62.50, 147.00, 74.00)),
("VCC3V3_ANT", (164.40, 50.40, 174.50, 70.00)), ("VCC0V95", (94.50, 56.00, 121.00, 103.00)),
("VCC3V0_RF", (145.00, 36.00, 174.50, 50.00)), ("VCC3V0_RF", (146.00, 39.80, 174.50, 50.50)),
("VCC3V0_DIG", (149.00, 35.20, 161.00, 39.60)),
("VCC3V0_DIG", (151.50, 44.80, 159.00, 48.60)),
("VCC3V3_ANT", (162.00, 53.50, 174.50, 63.20)),
) )
for priority, (net_name, bounds) in enumerate(islands, 1): for priority, (net_name, bounds) in enumerate(l4, 1):
add_zone(board, net_name, pcbnew.In3_Cu, bounds, priority) add_zone(board, net_name, pcbnew.In3_Cu, bounds, priority,
clearance=PLANE_CLEARANCE if net_name == "VCC0V95" else 0.20)
# Local BGA supply patches. L2/L7 remain uninterrupted ground planes; # The two big FPGA I/O rails do not get a plane layer of their own on an
# only the area below and immediately south of U1 is reserved on these # eight-layer board, so they are carried as islands on the two routing
# otherwise routable signal layers. # layers that have the room: VCC1V8 on L6 over its own loads (the BGA
add_zone(board, "VCC1V8", pcbnew.In4_Cu, (97.50, 60.50, 122.00, 101.00), 50) # config banks, the 1.8-V PMOD headers and the configuration flash), and
add_zone(board, "VCC3V3", pcbnew.In5_Cu, (97.50, 60.50, 140.00, 101.00), 50) # VCC3V3 on B.Cu under the three regions that draw it. Both layers still
# route signals around the islands, which is why the residual-connection
# stage refuses to cross a foreign pour.
l6 = (
("VCC1V8", (93.50, 44.00, 123.00, 92.00), 10),
("VCC1V8", (108.50, 91.50, 128.00, 106.00), 11),
("VCC1V8", (124.00, 20.50, 147.00, 28.50), 12),
)
for net_name, bounds, priority in l6:
add_zone(board, net_name, pcbnew.In5_Cu, bounds, priority,
clearance=PLANE_CLEARANCE)
bcu = (
("VCC3V3", (51.00, 45.00, 76.00, 92.00), 1),
("VCC3V3", (88.50, 22.50, 124.00, 86.00), 2),
("VCC3V3", (128.00, 56.50, 153.00, 104.50), 3),
)
for net_name, bounds, priority in bcu:
add_zone(board, net_name, pcbnew.B_Cu, bounds, priority,
clearance=PLANE_CLEARANCE)
# Areas the autorouter and the power stages must leave alone until
# finalize_routing.py installs the audited RF and USB copper. The "below"
# regions keep unrelated copper off the reference and routing layers under the
# GPS front end and the USB pair; the "corridor" regions additionally reserve
# F.Cu along the exact paths that finalize_routing.py later builds, since the
# critical nets themselves are not part of the Specctra export.
CRITICAL_KEEPOUTS_BELOW = (
("TEMP_RF_NO_DIGITAL_BELOW", (152.00, 36.00, 174.50, 43.70)),
("TEMP_USB_NO_CROSSING_BELOW", (26.00, 46.00, 39.60, 56.60)),
)
CRITICAL_KEEPOUTS_CORRIDOR = (
# GPS chain: SMA launch, bias tee, DC block, SAW, series R and MIXIN_HI.
("TEMP_RF_LINE", (154.40, 40.00, 174.60, 43.70)),
# USB pair: receptacle breakout and the mirrored-contact crossover.
("TEMP_USB_BREAKOUT", (27.30, 46.90, 30.65, 49.60)),
# USB pair: vertical channel into the TPD2EUSB30A clamp, and the short
# southward step D_P needs to clear the clamp's ground pad.
("TEMP_USB_CHANNEL", (29.60, 49.60, 30.68, 52.60)),
# USB pair: 45-degree run from the clamp to the series terminators.
("TEMP_USB_DIAGONAL", (30.68, 52.20, 35.30, 56.00)),
# USB pair: terminators and the matched legs into the FT4232H PHY.
("TEMP_USB_TERMINATORS", (34.80, 54.35, 39.45, 55.60)),
)
def add_temporary_critical_keepouts(board: pcbnew.BOARD) -> None: def add_temporary_critical_keepouts(board: pcbnew.BOARD) -> None:
"""Reserve the layers below RF and USB while Freerouting is running. """Reserve the RF and USB geometry while the rest of the board is routed.
F.Cu deliberately remains available so the autorouted critical nets reserve finalize_routing.py removes every TEMP_ area before installing the audited
their own top-layer corridors. The rule areas prevent unrelated signal copper, so nothing here reaches the manufactured board.
tracks and through vias from crossing underneath. finalize_routing.py
removes these temporary areas before installing the audited copper.
""" """
layers = pcbnew.LSET() # Every copper layer under F.Cu except the solid GND planes (L2/L7): the
for layer in (pcbnew.In2_Cu, pcbnew.In4_Cu, pcbnew.In5_Cu, pcbnew.B_Cu): # routing layers L3/L5/L6/B and the L4 power plane.
layers.AddLayer(layer) inner = (pcbnew.In2_Cu, pcbnew.In3_Cu, pcbnew.In4_Cu, pcbnew.In5_Cu,
for name, bounds in ( pcbnew.B_Cu)
("TEMP_RF_NO_DIGITAL_BELOW", (152.0, 36.0, 174.5, 49.5)), below = pcbnew.LSET()
("TEMP_USB_NO_CROSSING_BELOW", (22.0, 41.5, 44.5, 57.5))): for layer in inner:
below.AddLayer(layer)
corridor = pcbnew.LSET()
for layer in (pcbnew.F_Cu,) + inner:
corridor.AddLayer(layer)
for layers, group in ((below, CRITICAL_KEEPOUTS_BELOW),
(corridor, CRITICAL_KEEPOUTS_CORRIDOR)):
for name, bounds in group:
x0, y0, x1, y1 = bounds x0, y0, x1, y1 = bounds
area = pcbnew.ZONE(board) area = pcbnew.ZONE(board)
area.SetZoneName(name) area.SetZoneName(name)
@@ -517,7 +659,7 @@ def add_temporary_critical_keepouts(board: pcbnew.BOARD) -> None:
def add_documentation_graphics(board: pcbnew.BOARD) -> None: def add_documentation_graphics(board: pcbnew.BOARD) -> None:
add_text(board, "Ti375 GNSS / SCH16T", 105, 33.0, 1.1) add_text(board, "Ti375 GNSS / SCH16T", 105, 33.0, 1.1)
add_text(board, "REV A1 JLC08161H-3313 8L", 105, 35.2, 0.8) add_text(board, "REV A1 JLC08161H-3313 8L", 105, 35.2, 0.8)
add_text(board, "USB-C 5V", 31, 35.5, 0.9) add_text(board, "USB-C PWR", 37, 30, 0.8)
add_text(board, "EXT_5V", 28, 108.0, 0.9) add_text(board, "EXT_5V", 28, 108.0, 0.9)
add_text(board, "GPS L1/L1C ACTIVE", 162, 34, 0.9) add_text(board, "GPS L1/L1C ACTIVE", 162, 34, 0.9)
add_text(board, "BIAS J5: 3V3-OFF-5V", 163, 72, 0.8) add_text(board, "BIAS J5: 3V3-OFF-5V", 163, 72, 0.8)
@@ -533,31 +675,129 @@ def add_documentation_graphics(board: pcbnew.BOARD) -> None:
add_text(board, "PMOD4 1V8", 132, 30, 0.8) add_text(board, "PMOD4 1V8", 132, 30, 0.8)
STACKUP = '''\t\t(stackup STACKUP = ''' (stackup
\t\t\t(layer "F.SilkS" (type "Top Silk Screen")) (layer "F.SilkS"
\t\t\t(layer "F.Paste" (type "Top Solder Paste")) (type "Top Silk Screen")
\t\t\t(layer "F.Mask" (type "Top Solder Mask") (thickness 0.01)) )
\t\t\t(layer "F.Cu" (type "copper") (thickness 0.035)) (layer "F.Paste"
\t\t\t(layer "dielectric 1" (type "prepreg") (thickness 0.0994) (material "JLC 3313") (epsilon_r 4.1) (loss_tangent 0.02)) (type "Top Solder Paste")
\t\t\t(layer "L2_GND" (type "copper") (thickness 0.0152)) )
\t\t\t(layer "dielectric 2" (type "core") (thickness 0.1) (material "JLC FR-4 core") (epsilon_r 4.36) (loss_tangent 0.02)) (layer "F.Mask"
\t\t\t(layer "L3_SIG" (type "copper") (thickness 0.0152)) (type "Top Solder Mask")
\t\t\t(layer "dielectric 3" (type "prepreg") (thickness 0.3568) (material "JLC 3x2116") (epsilon_r 4.16) (loss_tangent 0.02)) (thickness 0.01)
\t\t\t(layer "L4_PWR" (type "copper") (thickness 0.0152)) )
\t\t\t(layer "dielectric 4" (type "core") (thickness 0.3) (material "JLC FR-4 core") (epsilon_r 4.41) (loss_tangent 0.02)) (layer "F.Cu"
\t\t\t(layer "L5_SIG" (type "copper") (thickness 0.0152)) (type "copper")
\t\t\t(layer "dielectric 5" (type "prepreg") (thickness 0.3568) (material "JLC 3x2116") (epsilon_r 4.16) (loss_tangent 0.02)) (thickness 0.035)
\t\t\t(layer "L6_SIG" (type "copper") (thickness 0.0152)) )
\t\t\t(layer "dielectric 6" (type "core") (thickness 0.1) (material "JLC FR-4 core") (epsilon_r 4.36) (loss_tangent 0.02)) (layer "dielectric 1"
\t\t\t(layer "L7_GND" (type "copper") (thickness 0.0152)) (type "prepreg")
\t\t\t(layer "dielectric 7" (type "prepreg") (thickness 0.0994) (material "JLC 3313") (epsilon_r 4.1) (loss_tangent 0.02)) (thickness 0.0994)
\t\t\t(layer "B.Cu" (type "copper") (thickness 0.035)) (material "JLC 3313")
\t\t\t(layer "B.Mask" (type "Bottom Solder Mask") (thickness 0.01)) (epsilon_r 4.1)
\t\t\t(layer "B.Paste" (type "Bottom Solder Paste")) (loss_tangent 0.02)
\t\t\t(layer "B.SilkS" (type "Bottom Silk Screen")) )
\t\t\t(copper_finish "ENIG") (layer "dielectric 2"
\t\t\t(dielectric_constraints yes) (type "copper")
\t\t)''' (thickness 0.0152)
(material "FR4")
(epsilon_r 4.5)
(loss_tangent 0.02)
)
(layer "dielectric 3"
(type "core")
(thickness 0.1)
(material "JLC FR-4 core")
(epsilon_r 4.36)
(loss_tangent 0.02)
)
(layer "dielectric 4"
(type "copper")
(thickness 0.0152)
(material "FR4")
(epsilon_r 4.5)
(loss_tangent 0.02)
)
(layer "dielectric 5"
(type "prepreg")
(thickness 0.3568)
(material "JLC 3x2116")
(epsilon_r 4.16)
(loss_tangent 0.02)
)
(layer "dielectric 6"
(type "copper")
(thickness 0.0152)
(material "FR4")
(epsilon_r 4.5)
(loss_tangent 0.02)
)
(layer "dielectric 7"
(type "core")
(thickness 0.3)
(material "JLC FR-4 core")
(epsilon_r 4.41)
(loss_tangent 0.02)
)
(layer "dielectric 8"
(type "copper")
(thickness 0.0152)
(material "FR4")
(epsilon_r 4.5)
(loss_tangent 0.02)
)
(layer "dielectric 9"
(type "prepreg")
(thickness 0.3568)
(material "JLC 3x2116")
(epsilon_r 4.16)
(loss_tangent 0.02)
)
(layer "dielectric 10"
(type "copper")
(thickness 0.0152)
(material "FR4")
(epsilon_r 4.5)
(loss_tangent 0.02)
)
(layer "dielectric 11"
(type "core")
(thickness 0.1)
(material "JLC FR-4 core")
(epsilon_r 4.36)
(loss_tangent 0.02)
)
(layer "dielectric 12"
(type "copper")
(thickness 0.0152)
(material "FR4")
(epsilon_r 4.5)
(loss_tangent 0.02)
)
(layer "dielectric 13"
(type "prepreg")
(thickness 0.0994)
(material "JLC 3313")
(epsilon_r 4.1)
(loss_tangent 0.02)
)
(layer "B.Cu"
(type "copper")
(thickness 0.035)
)
(layer "B.Mask"
(type "Bottom Solder Mask")
(thickness 0.01)
)
(layer "B.Paste"
(type "Bottom Solder Paste")
)
(layer "B.SilkS"
(type "Bottom Silk Screen")
)
(copper_finish "ENIG")
(dielectric_constraints yes)
)'''
def inject_stackup(path: Path) -> None: def inject_stackup(path: Path) -> None:
@@ -566,8 +806,6 @@ def inject_stackup(path: Path) -> None:
data = re.sub(r"\n\t\t\(stackup\n.*?\n\t\t\)\n", "\n" + STACKUP + "\n", data, count=1, flags=re.S) data = re.sub(r"\n\t\t\(stackup\n.*?\n\t\t\)\n", "\n" + STACKUP + "\n", data, count=1, flags=re.S)
else: else:
data = data.replace("\n\t(setup\n", "\n\t(setup\n" + STACKUP + "\n", 1) data = data.replace("\n\t(setup\n", "\n\t(setup\n" + STACKUP + "\n", 1)
data = data.replace("(allow_soldermask_bridges_in_footprints no)",
"(allow_soldermask_bridges_in_footprints yes)")
path.write_text(data) path.write_text(data)
@@ -585,8 +823,8 @@ def main() -> None:
title.SetTitle("Ti375 C529 GPS L1/L1C + SCH16T acquisition board") title.SetTitle("Ti375 C529 GPS L1/L1C + SCH16T acquisition board")
title.SetRevision("A0") title.SetRevision("A0")
title.SetCompany("JLCPCB manufacturing design") title.SetCompany("JLCPCB manufacturing design")
title.SetComment(0, "USB-C 5 V primary; protected EXT_5V; no back-feed") title.SetComment(0, "J7 USB-C 5 V or EXT_5V, muxed, never both; J1 debug-only, no back-feed")
title.SetComment(1, "JLC08161H-3313 exact eight-layer construction") title.SetComment(1, "JLC08161H-3313 exact eight-layer construction; F->L2 and L7->B keep 3313 for F.Cu impedance")
if not pcbnew.ZONE_FILLER(board).Fill(board.Zones()): if not pcbnew.ZONE_FILLER(board).Fill(board.Zones()):
raise RuntimeError("Initial inner-plane fill failed") raise RuntimeError("Initial inner-plane fill failed")
pcbnew.SaveBoard(str(BOARD_FILE), board) pcbnew.SaveBoard(str(BOARD_FILE), board)
+8 -7
View File
@@ -1,11 +1,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Compensate Freerouting 2.3.0's KiCad-10 DSN clearance scale mismatch. """Optionally scale only DSN clearance entries by an explicit factor.
KiCad exports widths and clearances in micrometres. Freerouting 2.3.0 keeps The bundled Freerouting 2.3.0 build reads KiCad 10 micrometre clearance values
the widths but internally imports DSN clearance values at one quarter of the directly, so the safe default is 1.0 (no scaling). This utility is retained for
intended physical value. Scaling only ``(clearance N)`` entries by four makes reproducible experiments with other router builds; never apply a non-unity
the router's physical rules match the source KiCad board. Coordinates, track factor without comparing the imported physical rule values first.
widths, vias, drills, padstacks, and keepouts are deliberately untouched.
""" """
from __future__ import annotations from __future__ import annotations
@@ -22,8 +21,10 @@ def main() -> None:
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("input", type=Path) parser.add_argument("input", type=Path)
parser.add_argument("output", type=Path) parser.add_argument("output", type=Path)
parser.add_argument("--factor", type=float, default=4.0) parser.add_argument("--factor", type=float, default=1.0)
args = parser.parse_args() args = parser.parse_args()
if args.factor <= 0:
raise ValueError("--factor must be positive")
source = args.input.read_text() source = args.input.read_text()
original = [float(match.group(2)) for match in CLEARANCE.finditer(source)] original = [float(match.group(2)) for match in CLEARANCE.finditer(source)]
+47 -3
View File
@@ -22,6 +22,10 @@ def mm(value: float) -> int:
return pcbnew.FromMM(value) return pcbnew.FromMM(value)
def centre_mm(position: pcbnew.VECTOR2I) -> tuple[float, float]:
return pcbnew.ToMM(position.x), pcbnew.ToMM(position.y)
def add_segment(board: pcbnew.BOARD, pad: pcbnew.PAD, def add_segment(board: pcbnew.BOARD, pad: pcbnew.PAD,
end: pcbnew.VECTOR2I, width_mm: float) -> None: end: pcbnew.VECTOR2I, width_mm: float) -> None:
track = pcbnew.PCB_TRACK(board) track = pcbnew.PCB_TRACK(board)
@@ -46,10 +50,25 @@ def add_via(board: pcbnew.BOARD, pad: pcbnew.PAD,
board.Add(via) board.Add(via)
def ball_grid(footprint: pcbnew.FOOTPRINT) -> tuple[dict, float, float, float, float]:
"""Every ball position, connected or not, plus the array extent."""
positions = {}
for pad in footprint.Pads():
p = pad.GetPosition()
positions[(round(pcbnew.ToMM(p.x), 2), round(pcbnew.ToMM(p.y), 2))] = pad
xs = [x for x, _ in positions]
ys = [y for _, y in positions]
return positions, min(xs), min(ys), max(xs), max(ys)
def main() -> None: def main() -> None:
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("input", nargs="?", type=Path, default=DEFAULT_INPUT) parser.add_argument("input", nargs="?", type=Path, default=DEFAULT_INPUT)
parser.add_argument("output", nargs="?", type=Path, default=DEFAULT_OUTPUT) parser.add_argument("output", nargs="?", type=Path, default=DEFAULT_OUTPUT)
parser.add_argument("--surface-rings", type=int, default=1,
help="How many outer ball rings escape on F.Cu without "
"a via. Each via removed here frees an inner-layer "
"channel and one anti-pad per plane.")
args = parser.parse_args() args = parser.parse_args()
board = pcbnew.LoadBoard(str(args.input)) board = pcbnew.LoadBoard(str(args.input))
@@ -58,12 +77,37 @@ def main() -> None:
raise RuntimeError("U1 Ti375 footprint is missing") raise RuntimeError("U1 Ti375 footprint is missing")
centre = fpga.GetPosition() centre = fpga.GetPosition()
occupied: set[tuple[int, int]] = set() occupied: set[tuple[int, int]] = set()
counts = {"power": 0, "signal": 0} counts = {"power": 0, "signal": 0, "surface": 0}
_grid, gx0, gy0, gx1, gy1 = ball_grid(fpga)
pitch = 0.8
for pad in fpga.Pads(): for pad in fpga.Pads():
if not pad.GetNetname(): if not pad.GetNetname():
continue continue
position = pad.GetPosition() position = pad.GetPosition()
x, y = pcbnew.ToMM(position.x), pcbnew.ToMM(position.y)
ring = min(round((x - gx0) / pitch), round((gx1 - x) / pitch),
round((y - gy0) / pitch), round((gy1 - y) / pitch))
is_power = pad.GetNetname() in POWER_NETS
# Signal balls on the outer rings leave straight out on F.Cu. They
# need no via at all, and the interstitial site they would have taken
# stays open as a channel for the balls further in. Power and ground
# balls always drop, because their whole purpose is to reach a plane.
if not is_power and ring < args.surface_rings:
dx = 0.0 if abs(x - centre_mm(centre)[0]) < 0.4 else (
1.0 if x > centre_mm(centre)[0] else -1.0)
dy = 0.0 if abs(y - centre_mm(centre)[1]) < 0.4 else (
1.0 if y > centre_mm(centre)[1] else -1.0)
if abs(x - centre_mm(centre)[0]) > abs(y - centre_mm(centre)[1]):
dy = 0.0
else:
dx = 0.0
reach = 1.6
end = pcbnew.VECTOR2I(position.x + mm(dx * reach),
position.y + mm(dy * reach))
add_segment(board, pad, end, 0.10)
counts["surface"] += 1
continue
# Move each ball toward its package quadrant. Centre-row/column balls # Move each ball toward its package quadrant. Centre-row/column balls
# use the positive direction, leaving a full 0.8-mm via grid without # use the positive direction, leaving a full 0.8-mm via grid without
# duplicated interstitial locations. # duplicated interstitial locations.
@@ -74,7 +118,6 @@ def main() -> None:
if key in occupied: if key in occupied:
raise RuntimeError(f"Duplicate dogbone via at {key} for U1.{pad.GetNumber()}") raise RuntimeError(f"Duplicate dogbone via at {key} for U1.{pad.GetNumber()}")
occupied.add(key) occupied.add(key)
is_power = pad.GetNetname() in POWER_NETS
add_segment(board, pad, via_position, 0.15 if is_power else 0.10) add_segment(board, pad, via_position, 0.15 if is_power else 0.10)
add_via(board, pad, via_position) add_via(board, pad, via_position)
counts["power" if is_power else "signal"] += 1 counts["power" if is_power else "signal"] += 1
@@ -86,7 +129,8 @@ def main() -> None:
source = args.input.with_suffix(suffix) source = args.input.with_suffix(suffix)
if source.exists(): if source.exists():
shutil.copyfile(source, args.output.with_suffix(suffix)) shutil.copyfile(source, args.output.with_suffix(suffix))
print(f"Installed {counts['power']} power/GND and {counts['signal']} signal dogbones") print(f"Installed {counts['power']} power/GND and {counts['signal']} signal "
f"dogbones, {counts['surface']} via-less surface escapes")
print(f"Saved {args.output}") print(f"Saved {args.output}")
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env python3
"""Summarise what a routed board actually contains.
DRC answers "is anything wrong"; this answers "is anything missing, and does
the copper match the rules it was routed under". It reports the still-open
connections per net, the track width actually used per net class (the Specctra
round trip is where widths silently drift), the layer split, and the via count.
"""
from __future__ import annotations
import argparse
from collections import Counter, defaultdict
from pathlib import Path
import pcbnew
def net_groups(board: pcbnew.BOARD, net: str) -> int:
"""Count the separate copper islands a net is currently split into."""
connectivity = board.GetConnectivity()
pads = [pad for fp in board.GetFootprints() for pad in fp.Pads()
if pad.GetNetname() == net]
seen: set[str] = set()
groups = 0
for pad in pads:
key = pad.m_Uuid.AsString()
if key in seen:
continue
members = [item for item in connectivity.GetConnectedItems(pad, 0)
if item.GetClass() == "PAD"]
for item in members:
seen.add(item.m_Uuid.AsString())
seen.add(key)
groups += 1
return groups
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("board", type=Path)
args = parser.parse_args()
board = pcbnew.LoadBoard(str(args.board))
board.BuildConnectivity()
net_class: dict[str, str] = {}
pad_count: Counter[str] = Counter()
for footprint in board.GetFootprints():
for pad in footprint.Pads():
name = pad.GetNetname()
if name:
net_class[name] = pad.GetNetClassName()
pad_count[name] += 1
widths: dict[str, Counter[float]] = defaultdict(Counter)
layers: Counter[str] = Counter()
vias = 0
for item in board.GetTracks():
if isinstance(item, pcbnew.PCB_VIA):
vias += 1
continue
name = item.GetNetname()
widths[net_class.get(name, "?")][round(pcbnew.ToMM(item.GetWidth()), 3)] += 1
layers[board.GetLayerName(item.GetLayer())] += 1
print(f"Board: {args.board}")
print(f"Unconnected endpoints reported by connectivity: "
f"{board.GetConnectivity().GetUnconnectedCount(True)}")
print(f"Tracks: {sum(layers.values())} Vias: {vias}")
print("\nTrack segments per layer:")
for layer, count in layers.most_common():
print(f" {layer:10s} {count:6d}")
print("\nTrack width used per net class (mm: segments):")
for klass in sorted(widths):
used = ", ".join(f"{width}: {count}"
for width, count in sorted(widths[klass].items()))
print(f" {klass:12s} {used}")
print("\nNets still split into more than one copper group:")
incomplete = []
for name in sorted(net_class):
if pad_count[name] < 2:
continue
groups = net_groups(board, name)
if groups > 1:
incomplete.append((groups, name))
for groups, name in sorted(incomplete, reverse=True):
print(f" {name:22s} {net_class[name]:12s} "
f"{pad_count[name]:3d} pads in {groups} groups")
print(f" ({len(incomplete)} nets incomplete)" if incomplete
else " none - every net is one connected group")
if __name__ == "__main__":
main()
+938
View File
@@ -0,0 +1,938 @@
#!/usr/bin/env python3
"""Finish the power distribution that planes and plane stitching cannot reach.
Three kinds of connection are left after `stitch_plane_pads.py`:
* pads whose rail has a plane, but not directly underneath them (a regulator
output capacitor just outside its island, a PMOD header on the board edge);
* rails that are too small to deserve a plane at all (`FTDI_1V8`,
`IMU_VREGD`, the antenna-bias nets), which are nevertheless excluded from
the Specctra export because their netclass is POWER;
* the local input/output loops around each LDO, where two different rails are
interleaved inside a couple of millimetres and no split plane can serve both.
None of these belong in the autorouter: it cannot see planes at all, so handing
it `GND` or `VCC3V3` would make it wire hundreds of pads together that the
plane already joins. This stage works from the real connectivity graph
instead. For every group of a rail that is still separate it either drops a
via onto the rail's own plane, or routes to the nearest other group with
45-degree geometry.
It runs in two phases. The first allows only plane drops and short direct
paths, for every rail; the second lets whatever is left take a detour through
an intermediate waypoint. Without that split the first rail in the list
spends the open space on long routes that a later rail needed for a short one.
Everything installed here is locked, and the same code runs again from
finalize_routing.py once the RF and USB corridors are released.
"""
from __future__ import annotations
import argparse
import math
import shutil
from collections import defaultdict
from pathlib import Path
import pcbnew
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_INPUT = ROOT / "hardware" / "board_plane_stitched.kicad_pcb"
DEFAULT_OUTPUT = ROOT / "hardware" / "board_power_local.kicad_pcb"
# Rails handled here. Signal nets, the deterministic converter switch nodes
# and the critical RF/USB nets are deliberately absent.
PLANE_NETS = (
"GND", "SYS5", "VCC0V95", "VCC1V8", "VCC3V3", "VCC3V3_AUX",
"VCC3V0_CLK", "VCC3V0_DIG", "VCC3V0_RF", "VCC3V3_ANT",
)
TRACE_NETS = (
"VBUS_RAW", "USB5_PROT", "EXT5_RAW", "EXT5_PROT", "USB_SHIELD",
"FTDI_1V8", "FTDI_VPHY", "FTDI_VPLL",
"IMU_3V3", "IMU_VREGA2", "IMU_VREGD",
"ANT_BIAS_SEL", "ANT_BIAS_SW",
)
TARGET_NETS = PLANE_NETS + TRACE_NETS
WIDE_NETS = {"GND", "SYS5", "VCC0V95", "VBUS_RAW", "USB5_PROT",
"EXT5_RAW", "EXT5_PROT"}
VIA_DIAMETER = 0.45
VIA_DRILL = 0.225
CLEARANCE = 0.13
# The rule value plus a rounding margin, used when completing signal nets
# through the Ti375 escape field where nothing wider will fit.
SIGNAL_CLEARANCE = 0.105
HOLE_TO_HOLE = 0.30
EDGE_CLEARANCE = 0.30
EDGE = (20.6, 20.6, 174.4, 109.4)
# Nets whose netclass demands more than the plain copper clearance. The local
# power router must respect the controlled-impedance corridors even though it
# never routes them itself.
STRICT_CLEARANCE = {
"RF_SMA": 0.20, "RF_PRE_SAW": 0.20, "RF_POST_SAW": 0.20, "MAX_MIXIN_HI": 0.20,
"USB_D_P": 0.15, "USB_D_N": 0.15, "FTDI_D_P": 0.15, "FTDI_D_N": 0.15,
}
# The edge-launch SMA lands sit on the board outline by construction; their
# ground return is built by finalize_routing.py's RF ground zone and via fence.
SKIP_REFS = {"J2"}
# Every ground land under the GPS front end is connected by that same RF ground
# zone and its via fence. Ordinary dogbones here would only add long stubs
# alongside the controlled-impedance line.
# The F.Cu ground pour over the whole GPS front end.
RF_GROUND_REGION = (152.0, 36.0, 174.5, 49.5)
# The band the RF line actually occupies, and the only part of the front end
# that has to stay clear of digital copper on the layers below F.Cu. The
# MAX2771's own digital pins sit south of it (pin rows at y = 44.5 .. 47.4)
# and need those layers to escape the package at all.
RF_NO_DIGITAL_REGION = (152.0, 36.0, 174.5, 43.7)
# Nothing but ground and the MAX2771's own supplies may run under the GPS front
# end on a layer other than F.Cu. This router is the one stage that runs after
# finalize_routing.py has deleted the temporary keepout, so it has to carry the
# restriction itself; without it the deterministic completion is free to drop a
# charge-pump or control net onto L3 directly beneath the 50-ohm line.
RF_REGION_LAYERS = (pcbnew.In2_Cu, pcbnew.In3_Cu, pcbnew.In4_Cu, pcbnew.In5_Cu,
pcbnew.B_Cu)
def rf_region_forbids(net: str, layer: int | None) -> bool:
"""True when this net may not occupy the RF region on this layer.
`layer` of None means a through via, which is present on every layer and so
is judged as if it were on the inner ones.
"""
if net in TARGET_NETS:
return False
if layer is not None and layer not in RF_REGION_LAYERS:
return False
return True
def in_rf_region(position: tuple[float, float], margin: float = 0.0) -> bool:
return (RF_NO_DIGITAL_REGION[0] - margin <= position[0] <= RF_NO_DIGITAL_REGION[2] + margin and
RF_NO_DIGITAL_REGION[1] - margin <= position[1] <= RF_NO_DIGITAL_REGION[3] + margin)
MAX_PLANE_REACH = 7.0
# A dogbone via never sits further than this from the pad it escapes.
ESCAPE_VIA_REACH = 0.8
MAX_JOIN_LENGTH = 80.0
def mm(value: float) -> int:
return pcbnew.FromMM(value)
def to_mm(value: int) -> float:
return pcbnew.ToMM(value)
def point(xy: tuple[float, float]) -> pcbnew.VECTOR2I:
return pcbnew.VECTOR2I(mm(xy[0]), mm(xy[1]))
# Widths used when this stage finishes leftover signal nets. They match the
# KiCad net classes so the copper is indistinguishable from the autorouted
# result; the controlled-impedance classes are absent because finalize_routing
# builds those by hand.
NETCLASS_WIDTH = {"Default": 0.12, "BGA_ESCAPE": 0.10, "CLOCK": 0.15}
# Per-class routing clearance. CLOCK keeps its wider figure; the others drop
# to the rule value so they still fit the Ti375 escape channels.
NETCLASS_CLEARANCE = {"Default": 0.105, "BGA_ESCAPE": 0.105, "CLOCK": 0.155}
SKIP_CLASSES = frozenset({"RF50_CPWG", "USB90_DIFF"})
def net_width(net: str) -> float:
return 0.40 if net in WIDE_NETS else 0.25
def segment_distance(p: tuple[float, float], a: tuple[float, float],
b: tuple[float, float]) -> float:
dx, dy = b[0] - a[0], b[1] - a[1]
length = dx * dx + dy * dy
if length == 0.0:
return math.hypot(p[0] - a[0], p[1] - a[1])
t = max(0.0, min(1.0, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / length))
return math.hypot(p[0] - (a[0] + t * dx), p[1] - (a[1] + t * dy))
def _seg_aabb_distance(start: tuple[float, float], end: tuple[float, float],
x0: float, y0: float, x1: float, y1: float) -> float:
"""Exact distance from a segment to an axis-aligned box (0 if they touch)."""
for probe in (start, end):
if x0 <= probe[0] <= x1 and y0 <= probe[1] <= y1:
return 0.0
corners = ((x0, y0), (x1, y0), (x1, y1), (x0, y1))
return min(segments_distance(start, end, corners[i], corners[(i + 1) % 4])
for i in range(4))
def pad_point_distance(shape: tuple[float, float, float, float, float],
probe: tuple[float, float]) -> float:
"""Distance from a point to a pad, as an oriented rounded rectangle.
`shape` is (cx, cy, hx, hy, r): centre, half extents and corner radius.
A circle is hx = hy = 0 with r = radius; a plain rectangle is r = 0. This
replaces the old bounding-box test, which measured a diagonal neighbour
from the box edge instead of the land itself and so rejected every legal
0.8-mm BGA dogbone site.
"""
cx, cy, hx, hy, r = shape
dx = max(abs(probe[0] - cx) - hx, 0.0)
dy = max(abs(probe[1] - cy) - hy, 0.0)
return max(math.hypot(dx, dy) - r, 0.0)
def pad_segment_distance(shape: tuple[float, float, float, float, float],
start: tuple[float, float],
end: tuple[float, float]) -> float:
"""Distance from a segment to a pad modelled as a rounded rectangle."""
cx, cy, hx, hy, r = shape
return max(_seg_aabb_distance(start, end, cx - hx, cy - hy,
cx + hx, cy + hy) - r, 0.0)
def pad_shape(pad: pcbnew.PAD) -> tuple[float, float, float, float, float]:
"""Analytic (cx, cy, hx, hy, r) for a pad, exact for every shape on this
board (circle, oval, rectangle, rounded rectangle) at 0 or 90 degrees."""
position = pad.GetPosition()
cx, cy = to_mm(position.x), to_mm(position.y)
size_x, size_y = to_mm(pad.GetSizeX()) / 2, to_mm(pad.GetSizeY()) / 2
if round(pad.GetOrientationDegrees()) % 180 == 90:
size_x, size_y = size_y, size_x
shape = pad.GetShape()
if shape == pcbnew.PAD_SHAPE_CIRCLE:
return (cx, cy, 0.0, 0.0, min(size_x, size_y))
if shape == pcbnew.PAD_SHAPE_OVAL:
radius = min(size_x, size_y)
return (cx, cy, size_x - radius, size_y - radius, radius)
if shape == pcbnew.PAD_SHAPE_ROUNDRECT:
radius = min(to_mm(pad.GetRoundRectCornerRadius()), size_x, size_y)
return (cx, cy, size_x - radius, size_y - radius, radius)
# rectangle, trapezoid, custom and anything else: the bounding box, which
# is what the model used everywhere before.
return (cx, cy, size_x, size_y, 0.0)
def segments_distance(a0: tuple[float, float], a1: tuple[float, float],
b0: tuple[float, float], b1: tuple[float, float]) -> float:
"""Minimum distance between two segments, including the crossing case."""
def cross(o, a, b):
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
d1, d2 = cross(a0, a1, b0), cross(a0, a1, b1)
d3, d4 = cross(b0, b1, a0), cross(b0, b1, a1)
if ((d1 > 0) != (d2 > 0)) and ((d3 > 0) != (d4 > 0)):
return 0.0
return min(segment_distance(b0, a0, a1), segment_distance(b1, a0, a1),
segment_distance(a0, b0, b1), segment_distance(a1, b0, b1))
class Obstacles:
"""Other-net copper, indexed on a coarse grid for quick local lookups."""
CELL = 5.0
def __init__(self, board: pcbnew.BOARD,
escape_refs: frozenset[str] | None = None,
escape_clearance: float = 0.10) -> None:
# Footprints whose pads carry a DRC rule that overrides the net-class
# clearance -- on this board the "Ti375 0.8-mm BGA escape clearance"
# rule in board.kicad_dru, which allows 0.10 mm whenever one of the two
# objects belongs to U1. Opt-in: every existing caller keeps the plain
# net-class behaviour.
self.escape_refs = frozenset(escape_refs or ())
self.escape_clearance = escape_clearance
self.board = board
self.cells: dict[tuple[int, int], list] = defaultdict(list)
self.holes: list[tuple[tuple[float, float], float]] = []
for footprint in board.GetFootprints():
for pad in footprint.Pads():
box = pad.GetBoundingBox()
bounds = (to_mm(box.GetX()), to_mm(box.GetY()),
to_mm(box.GetRight()), to_mm(box.GetBottom()))
self._insert(("pad", pad.GetNetname(), pad_shape(pad),
pad.GetLayerSet(),
footprint.GetReference()), bounds)
drill = max(to_mm(pad.GetDrillSizeX()), to_mm(pad.GetDrillSizeY()))
if drill > 0:
position = pad.GetPosition()
self.holes.append(((to_mm(position.x), to_mm(position.y)),
drill / 2))
for item in board.GetTracks():
if isinstance(item, pcbnew.PCB_VIA):
position = (to_mm(item.GetPosition().x), to_mm(item.GetPosition().y))
radius = to_mm(item.GetWidth(pcbnew.F_Cu)) / 2
self.add_via(item.GetNetname(), position, radius,
to_mm(item.GetDrill()) / 2)
else:
start = (to_mm(item.GetStart().x), to_mm(item.GetStart().y))
end = (to_mm(item.GetEnd().x), to_mm(item.GetEnd().y))
self.add_track(item.GetNetname(), start, end,
to_mm(item.GetWidth()), item.GetLayer())
# Only areas that actually forbid copper block routing. Rule areas
# that exist purely so a custom DRC rule can name a region (see
# finalize_routing.add_named_rule_areas) must not stop anything.
self.track_areas = [zone for zone in board.Zones()
if zone.GetIsRuleArea() and zone.GetDoNotAllowTracks()]
self.via_areas = [zone for zone in board.Zones()
if zone.GetIsRuleArea() and zone.GetDoNotAllowVias()]
def _keys(self, bounds: tuple[float, float, float, float]):
x0 = int(math.floor(bounds[0] / self.CELL))
y0 = int(math.floor(bounds[1] / self.CELL))
x1 = int(math.floor(bounds[2] / self.CELL))
y1 = int(math.floor(bounds[3] / self.CELL))
for x in range(x0 - 1, x1 + 2):
for y in range(y0 - 1, y1 + 2):
yield x, y
def _insert(self, entry, bounds) -> None:
for key in self._keys(bounds):
self.cells[key].append(entry)
def add_via(self, net: str, position: tuple[float, float], radius: float,
drill_radius: float = VIA_DRILL / 2) -> None:
bounds = (position[0] - radius, position[1] - radius,
position[0] + radius, position[1] + radius)
self._insert(("via", net, position, radius), bounds)
self.holes.append((position, drill_radius))
def add_track(self, net: str, start: tuple[float, float],
end: tuple[float, float], width: float, layer: int) -> None:
bounds = (min(start[0], end[0]) - width, min(start[1], end[1]) - width,
max(start[0], end[0]) + width, max(start[1], end[1]) + width)
self._insert(("track", net, start, end, width / 2, layer), bounds)
def _nearby(self, bounds):
seen = set()
for key in self._keys(bounds):
for entry in self.cells.get(key, ()):
if id(entry) in seen:
continue
seen.add(id(entry))
yield entry
def rule_area_blocks(self, position: tuple[float, float], layer: int | None,
radius: float = 0.0) -> bool:
"""Test a disc, not a point: copper close to a rule area still violates it.
A layer of None means a through via, which is checked against the areas
that forbid vias on any layer.
"""
probes = [position]
for step in range(8):
angle = math.radians(45 * step)
probes.append((position[0] + radius * math.cos(angle),
position[1] + radius * math.sin(angle)))
areas = self.via_areas if layer is None else self.track_areas
for zone in areas:
if layer is not None and not zone.GetLayerSet().Contains(layer):
continue
outline = zone.Outline()
if any(outline.Contains(point(probe)) for probe in probes):
return True
return False
def hole_clear(self, position: tuple[float, float],
drill_radius: float = VIA_DRILL / 2) -> bool:
for other, other_radius in self.holes:
gap = math.hypot(position[0] - other[0], position[1] - other[1])
if gap < drill_radius + other_radius + HOLE_TO_HOLE:
return False
return True
def track_clear(self, net: str, start: tuple[float, float],
end: tuple[float, float], width: float, layer: int,
clearance_floor: float | None = None) -> bool:
"""`clearance_floor` overrides the routing margin with the bare rule
value, for callers that are checking existing copper rather than
placing new copper with room to spare."""
floor = CLEARANCE if clearance_floor is None else clearance_floor
half = width / 2
margin_to_edge = half + EDGE_CLEARANCE
for probe in (start, end):
if not (EDGE[0] + margin_to_edge <= probe[0] <= EDGE[2] - margin_to_edge and
EDGE[1] + margin_to_edge <= probe[1] <= EDGE[3] - margin_to_edge):
return False
forbidden = rf_region_forbids(net, layer)
for fraction in range(0, 11):
sample = (start[0] + (end[0] - start[0]) * fraction / 10,
start[1] + (end[1] - start[1]) * fraction / 10)
if self.rule_area_blocks(sample, layer, half + CLEARANCE):
return False
if forbidden and in_rf_region(sample, half + CLEARANCE):
return False
bounds = (min(start[0], end[0]) - 2, min(start[1], end[1]) - 2,
max(start[0], end[0]) + 2, max(start[1], end[1]) + 2)
for entry in self._nearby(bounds):
if entry[1] == net:
continue
clearance = max(floor, STRICT_CLEARANCE.get(entry[1], 0.0))
if entry[0] == "pad":
_, _, shape, layers, ref = entry
if not layers.Contains(layer):
continue
if ref in self.escape_refs:
clearance = self.escape_clearance
if pad_segment_distance(shape, start, end) < half + clearance:
return False
elif entry[0] == "via":
_, _, position, radius = entry
if segment_distance(position, start, end) < half + radius + clearance:
return False
else:
_, _, other_start, other_end, other_half, other_layer = entry
if other_layer != layer:
continue
if segments_distance(start, end, other_start,
other_end) < half + other_half + clearance:
return False
return True
def via_clear(self, net: str, position: tuple[float, float],
clearance_floor: float | None = None) -> bool:
floor = CLEARANCE if clearance_floor is None else clearance_floor
radius = VIA_DIAMETER / 2
if not (EDGE[0] + radius <= position[0] <= EDGE[2] - radius and
EDGE[1] + radius <= position[1] <= EDGE[3] - radius):
return False
# A through via is present on every layer, so any rule area blocks it.
if self.rule_area_blocks(position, None, radius + CLEARANCE):
return False
if rf_region_forbids(net, None) and in_rf_region(position, radius + CLEARANCE):
return False
if not self.hole_clear(position):
return False
bounds = (position[0] - 2, position[1] - 2, position[0] + 2, position[1] + 2)
for entry in self._nearby(bounds):
if entry[1] == net:
continue
clearance = max(floor, STRICT_CLEARANCE.get(entry[1], 0.0))
if entry[0] == "pad":
_, _, shape, _layers, ref = entry
if ref in self.escape_refs:
clearance = self.escape_clearance
if pad_point_distance(shape, position) < radius + clearance:
return False
elif entry[0] == "via":
_, _, other, other_radius = entry
if math.hypot(position[0] - other[0],
position[1] - other[1]) < radius + other_radius + clearance:
return False
else:
_, _, start, end, half, _layer = entry
if segment_distance(position, start, end) < radius + half + clearance:
return False
return True
def diagonal_paths(a: tuple[float, float],
b: tuple[float, float]) -> list[list[tuple[float, float]]]:
"""Two-segment 45-degree routes between two points, shortest first."""
dx, dy = b[0] - a[0], b[1] - a[1]
if abs(dx) < 1e-6 or abs(dy) < 1e-6 or abs(abs(dx) - abs(dy)) < 1e-6:
return [[a, b]]
sx = math.copysign(1.0, dx)
sy = math.copysign(1.0, dy)
run = abs(abs(dx) - abs(dy))
if abs(dx) > abs(dy):
diagonal_first = [a, (a[0] + sx * abs(dy), a[1] + dy), b]
straight_first = [a, (a[0] + sx * run, a[1]), b]
else:
diagonal_first = [a, (a[0] + dx, a[1] + sy * abs(dx)), b]
straight_first = [a, (a[0], a[1] + sy * run), b]
return [diagonal_first, straight_first,
[a, (a[0], b[1]), b], [a, (b[0], a[1]), b]]
def detour_paths(a: tuple[float, float], b: tuple[float, float],
step: float = 2.5, margin: float = 12.0,
limit: int = 96) -> list[list[tuple[float, float]]]:
"""Routes that reach b through one intermediate point, shortest first.
Two segments only ever get around an obstacle by luck. Sampling a waypoint
grid over the span between the two ends, and joining a-to-waypoint and
waypoint-to-b with the same 45-degree geometry, is enough to get a supply
trace past the components between an LDO and the far end of its rail.
"""
x0, x1 = sorted((a[0], b[0]))
y0, y1 = sorted((a[1], b[1]))
candidates = []
steps_x = int((x1 - x0 + 2 * margin) / step) + 1
steps_y = int((y1 - y0 + 2 * margin) / step) + 1
for ix in range(steps_x):
for iy in range(steps_y):
way = (round(x0 - margin + ix * step, 3),
round(y0 - margin + iy * step, 3))
if not (EDGE[0] <= way[0] <= EDGE[2] and EDGE[1] <= way[1] <= EDGE[3]):
continue
length = (math.hypot(way[0] - a[0], way[1] - a[1]) +
math.hypot(b[0] - way[0], b[1] - way[1]))
candidates.append((length, way))
candidates.sort()
routes = []
for _length, way in candidates[:limit]:
first = diagonal_paths(a, way)[0]
second = diagonal_paths(way, b)[0]
routes.append(first + second[1:])
return routes
class LocalRouter:
def __init__(self, board: pcbnew.BOARD) -> None:
self.board = board
self.obstacles = Obstacles(board)
self.installed = 0
self.vias = 0
self.widths: dict[str, float] = {}
# Power rails are routed with a margin over the rule. Signal
# completion has to drop to the rule itself: a 0.12-mm trace needs
# 0.385 mm of the 0.400-mm channel between two Ti375 escape vias, and
# at 0.13 mm it would need 0.415 mm and never fit.
self.clearance = CLEARANCE
self.clearances: dict[str, float] = {}
# Taken from the board, not written down here: the stackup gained a
# fourth signal layer when L5 stopped being a power plane, and a fixed
# list would have gone on routing everything through the old three.
self.routing_layers = [layer for layer in board.GetEnabledLayers().CuStack()
if board.GetLayerType(layer) == pcbnew.LT_SIGNAL]
def width_for(self, net: str) -> float:
return self.widths.get(net, net_width(net))
def clearance_for(self, net: str) -> float:
return self.clearances.get(net, self.clearance)
def path_clear(self, net: str, vertices: list[tuple[float, float]],
width: float, layer: int) -> bool:
for start, end in zip(vertices, vertices[1:]):
if start == end:
continue
if not self.obstacles.track_clear(net, start, end, width, layer,
clearance_floor=self.clearance_for(net)):
return False
return True
def first_clear(self, net: str, a: tuple[float, float], b: tuple[float, float],
width: float, layer: int,
detour: bool = True) -> list[tuple[float, float]] | None:
"""The shortest clear route from a to b on one layer, or None."""
for vertices in diagonal_paths(a, b):
if self.path_clear(net, vertices, width, layer):
return vertices
if not detour:
return None
for vertices in detour_paths(a, b):
if self.path_clear(net, vertices, width, layer):
return vertices
return None
def commit(self, net: str, vertices: list[tuple[float, float]],
width: float, layer: int) -> None:
for start, end in zip(vertices, vertices[1:]):
if start == end:
continue
track = pcbnew.PCB_TRACK(self.board)
track.SetStart(point(start))
track.SetEnd(point(end))
track.SetWidth(mm(width))
track.SetLayer(layer)
track.SetNet(self.board.FindNet(net))
track.SetLocked(True)
self.board.Add(track)
self.obstacles.add_track(net, start, end, width, layer)
self.installed += 1
def commit_via(self, net: str, position: tuple[float, float]) -> None:
via = pcbnew.PCB_VIA(self.board)
via.SetPosition(point(position))
via.SetWidth(mm(VIA_DIAMETER))
via.SetDrill(mm(VIA_DRILL))
via.SetLayerPair(pcbnew.F_Cu, pcbnew.B_Cu)
via.SetNet(self.board.FindNet(net))
via.SetLocked(True)
self.board.Add(via)
self.obstacles.add_via(net, position, VIA_DIAMETER / 2)
self.vias += 1
def connect(self, net: str, a: tuple[float, float], a_layer: int,
b: tuple[float, float], b_layer: int,
detour: bool = True) -> bool:
"""Route a to b, changing layer through a via when that is what fits."""
width = self.width_for(net)
if a_layer == b_layer:
vertices = self.first_clear(net, a, b, width, a_layer, detour)
if vertices is not None:
self.commit(net, vertices, width, a_layer)
return True
for layer in self.routing_layers:
if layer == a_layer and layer == b_layer:
continue
for via_a, via_b in self.via_options(net, a, a_layer, b, b_layer, layer):
plan = []
if via_a is not None:
if not self.path_clear(net, [a, via_a], width, a_layer):
continue
plan.append(("track", [a, via_a], a_layer))
plan.append(("via", via_a))
start = via_a
else:
start = a
end = via_b if via_b is not None else b
middle = self.first_clear(net, start, end, width, layer, detour)
if middle is None:
continue
plan.append(("track", middle, layer))
if via_b is not None:
if not self.path_clear(net, [via_b, b], width, b_layer):
continue
plan.append(("via", via_b))
plan.append(("track", [via_b, b], b_layer))
for entry in plan:
if entry[0] == "via":
self.commit_via(net, entry[1])
else:
self.commit(net, entry[1], width, entry[2])
return True
return False
def via_options(self, net: str, a: tuple[float, float], a_layer: int,
b: tuple[float, float], b_layer: int, layer: int):
"""Offer via drop points just outside each endpoint's own pad."""
escapes_a = [None] if a_layer == layer else self.escape_points(net, a, b)
escapes_b = [None] if b_layer == layer else self.escape_points(net, b, a)
for via_a in escapes_a[:4]:
for via_b in escapes_b[:4]:
yield via_a, via_b
def escape_points(self, net: str, origin: tuple[float, float],
towards: tuple[float, float]) -> list[tuple[float, float]]:
heading = math.atan2(towards[1] - origin[1], towards[0] - origin[0])
options = []
for radius in (0.65, 0.95, 1.35):
for step in range(8):
angle = math.radians(45 * step)
candidate = (round(origin[0] + radius * math.cos(angle), 3),
round(origin[1] + radius * math.sin(angle), 3))
if not self.obstacles.via_clear(net, candidate, self.clearance_for(net)):
continue
score = -math.cos(angle - heading) * 10 + radius
options.append((score, candidate))
options.sort()
return [candidate for _score, candidate in options]
def reach_plane(self, net: str, zones: list[pcbnew.ZONE],
terminals: list[tuple[tuple[float, float], int]]) -> bool:
"""Drop a via from one of this group's pads onto the rail's own plane."""
width = self.width_for(net)
best = None
for origin, layer in terminals:
for radius in [x * 0.4 for x in range(2, int(MAX_PLANE_REACH / 0.4) + 1)]:
for step in range(8):
angle = math.radians(45 * step)
candidate = (round(origin[0] + radius * math.cos(angle), 3),
round(origin[1] + radius * math.sin(angle), 3))
probe = point(candidate)
if not any(zone.HitTestFilledArea(zone.GetLayer(), probe)
for zone in zones):
continue
if not self.obstacles.via_clear(net, candidate, self.clearance_for(net)):
continue
if not self.path_clear(net, [origin, candidate], width, layer):
continue
if best is None or radius < best[0]:
best = (radius, origin, candidate, layer)
break
if best is not None and best[0] <= radius:
break
if best is None:
return False
_radius, origin, candidate, layer = best
self.commit(net, [origin, candidate], width, layer)
self.commit_via(net, candidate)
return True
def pad_layer(pad: pcbnew.PAD) -> int:
layers = pad.GetLayerSet().Seq()
return layers[0] if layers else pcbnew.F_Cu
def groups_for_net(board: pcbnew.BOARD, net: str) -> list[list[pcbnew.PAD]]:
connectivity = board.GetConnectivity()
pads = [pad for fp in board.GetFootprints() for pad in fp.Pads()
if pad.GetNetname() == net]
seen: set[str] = set()
groups: list[list[pcbnew.PAD]] = []
for pad in pads:
key = pad.m_Uuid.AsString()
if key in seen:
continue
members = [item for item in connectivity.GetConnectedItems(pad, 0)
if item.GetClass() == "PAD"] or [pad]
for item in members:
seen.add(item.m_Uuid.AsString())
seen.add(key)
groups.append([item.Cast() if hasattr(item, "Cast") else item
for item in members])
return groups
def escape_via_map(board: pcbnew.BOARD) -> dict[str, list[tuple[float, float]]]:
result: dict[str, list[tuple[float, float]]] = defaultdict(list)
for item in board.GetTracks():
if isinstance(item, pcbnew.PCB_VIA):
position = item.GetPosition()
result[item.GetNetname()].append(
(round(to_mm(position.x), 4), round(to_mm(position.y), 4)))
return result
def terminals_of(group: list[pcbnew.PAD], net: str,
vias: dict[str, list[tuple[float, float]]] | None = None,
inner_layers: tuple[int, ...] = (pcbnew.B_Cu,),
) -> list[tuple[tuple[float, float], int]]:
"""Points this group can be routed from, with the layer each one sits on.
A pad that already has a dogbone via contributes that via as well. Under
the Ti375 the escape vias are the only usable terminals: an isolated bank
supply ball has no plane left around its via (every neighbouring ball's
anti-pad eats it), and F.Cu there is solid escape fan-out, so the one route
that exists is a short hop to the neighbouring same-net via on B.Cu.
`inner_layers` is the set of routing layers such a via can be picked up on;
it comes from the board so a stackup change cannot silently strand one.
"""
result = []
for pad in group:
parent = pad.GetParentFootprint()
if parent is not None and parent.GetReference() in SKIP_REFS:
continue
position = pad.GetPosition()
xy = (round(to_mm(position.x), 4), round(to_mm(position.y), 4))
if net == "GND" and (RF_GROUND_REGION[0] <= xy[0] <= RF_GROUND_REGION[2] and
RF_GROUND_REGION[1] <= xy[1] <= RF_GROUND_REGION[3]):
continue
result.append((xy, pad_layer(pad)))
for via in (vias or {}).get(net, ()):
if math.hypot(via[0] - xy[0], via[1] - xy[1]) <= ESCAPE_VIA_REACH:
for layer in inner_layers:
result.append((via, layer))
return result
def on_plane(board: pcbnew.BOARD, group: list[pcbnew.PAD],
zones: list[pcbnew.ZONE]) -> bool:
"""True when this group already sits on one of the rail's own zones.
Without this test a group that reached its plane but did not thereby merge
with any other group - because no other group is on that island - reports
progress on every pass. route_net then keeps calling reach_plane, which
keeps finding the next free spot one step further out, and the rail ends up
with a cloud of dozens of redundant vias around a single pad.
"""
if not zones:
return False
wanted = {zone.m_Uuid.AsString() for zone in zones}
connectivity = board.GetConnectivity()
for pad in group:
for item in connectivity.GetConnectedItems(pad, 0):
if item.GetClass() == "ZONE" and item.m_Uuid.AsString() in wanted:
return True
return False
def route_net(router: LocalRouter, board: pcbnew.BOARD, net: str,
zones: list[pcbnew.ZONE], detour: bool = True) -> tuple[int, int]:
"""Return the number of groups joined and the number left unresolved."""
joined = 0
stalled = 0
previous = None
for _pass in range(60):
board.BuildConnectivity()
groups = groups_for_net(board, net)
if len(groups) <= 1:
break
# A pass that claims progress without reducing the group count has not
# actually connected anything new. Allow a couple of those - a plane
# drop can legitimately need one - then stop rather than pile on copper.
if previous is not None and len(groups) >= previous:
stalled += 1
if stalled >= 2:
break
else:
stalled = 0
previous = len(groups)
vias = escape_via_map(board)
inner = tuple(layer for layer in router.routing_layers
if layer != pcbnew.F_Cu)
terminals = [terminals_of(group, net, vias, inner) for group in groups]
sizes = [len(group) for group in groups]
main = sizes.index(max(sizes))
progress = False
for index, group_terminals in enumerate(terminals):
if index == main:
continue
if (zones and not on_plane(board, groups[index], zones)
and router.reach_plane(net, zones, group_terminals)):
joined += 1
progress = True
break
candidates = []
for other, other_terminals in enumerate(terminals):
if other == index:
continue
for a, a_layer in group_terminals:
for b, b_layer in other_terminals:
distance = math.hypot(a[0] - b[0], a[1] - b[1])
if distance <= MAX_JOIN_LENGTH:
candidates.append((distance, a, a_layer, b, b_layer))
candidates.sort(key=lambda entry: entry[0])
for _distance, a, a_layer, b, b_layer in candidates[:24]:
if router.connect(net, a, a_layer, b, b_layer, detour):
joined += 1
progress = True
break
if progress:
break
if not progress:
break
board.BuildConnectivity()
return joined, len(groups_for_net(board, net)) - 1
def signal_nets(board: pcbnew.BOARD,
classes: frozenset[str] | None = None) -> dict[str, float]:
"""Routable signal nets and the track width their net class asks for."""
result: dict[str, float] = {}
for footprint in board.GetFootprints():
for pad in footprint.Pads():
name = pad.GetNetname()
if not name or name in TARGET_NETS or name in result:
continue
klass = pad.GetNetClassName()
if klass in SKIP_CLASSES or (classes and klass not in classes):
continue
width = NETCLASS_WIDTH.get(klass)
if width is not None:
result[name] = width
return result
def signal_clearances(board: pcbnew.BOARD,
classes: frozenset[str] | None = None) -> dict[str, float]:
result: dict[str, float] = {}
for footprint in board.GetFootprints():
for pad in footprint.Pads():
name = pad.GetNetname()
if not name or name in result:
continue
klass = pad.GetNetClassName()
if classes and klass not in classes:
continue
value = NETCLASS_CLEARANCE.get(klass)
if value is not None:
result[name] = value
return result
def complete(board: pcbnew.BOARD, verbose: bool = True,
signals: bool = False,
signal_classes: frozenset[str] | None = None) -> dict[str, int]:
"""Run the local power completion on an already-filled board.
finalize_routing.py calls this a second time once the temporary RF and USB
rule areas are gone, which is the only moment the pads inside those
corridors can be reached. With `signals` it then makes the same two passes
over ordinary signal nets, to close the connections the autorouter gave up
on; those use their net class width and are locked like everything else.
"""
router = LocalRouter(board)
zones_by_net: dict[str, list[pcbnew.ZONE]] = defaultdict(list)
for zone in board.Zones():
if not zone.GetIsRuleArea():
zones_by_net[zone.GetNetname()].append(zone)
# Two phases. Everything that can be closed with a plane drop or a short
# direct path is done first, for every rail; only then are the leftovers
# allowed to take a detour. Otherwise the first rail in the list spends
# the free space on long routes that a later rail needed for a short one.
remaining: dict[str, int] = {}
for detour in (False, True):
remaining = {}
for net in TARGET_NETS:
if board.FindNet(net) is None:
continue
joined, left = route_net(router, board, net,
zones_by_net.get(net, []), detour)
if verbose and detour and (joined or left):
print(f"{net:14s} {left} groups still separate")
if left:
remaining[net] = left
if signals:
# The Specctra import normally completes hundreds of signal nets. A
# full BuildConnectivity inside route_net() for every already-finished
# net made this post-pass spend several minutes proving the same fact
# twice. Build once, retain only nets that are genuinely split, and
# run the unchanged router over that exact residual set.
board.BuildConnectivity()
all_widths = signal_nets(board, signal_classes)
router.widths = {
name: width for name, width in all_widths.items()
if len(groups_for_net(board, name)) > 1
}
router.clearance = SIGNAL_CLEARANCE
router.clearances = {
name: clearance
for name, clearance in signal_clearances(board, signal_classes).items()
if name in router.widths
}
closed = 0
for detour in (False, True):
for net in sorted(router.widths):
joined, left = route_net(router, board, net, [], detour)
closed += joined
if left:
remaining[net] = left
else:
remaining.pop(net, None)
if verbose:
print(f"Signal completion joined {closed} groups")
if verbose:
print(f"Installed {router.installed} tracks and {router.vias} vias")
print("Unresolved:",
", ".join(f"{n}={k}" for n, k in sorted(remaining.items())) or "none")
return remaining
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("input", nargs="?", type=Path, default=DEFAULT_INPUT)
parser.add_argument("output", nargs="?", type=Path, default=DEFAULT_OUTPUT)
parser.add_argument("--clocks", action="store_true",
help="Also route the CLOCK class here, before the "
"autorouter runs, so those nets are fixed copper "
"by the time it starts.")
args = parser.parse_args()
board = pcbnew.LoadBoard(str(args.input))
if not pcbnew.ZONE_FILLER(board).Fill(board.Zones()):
raise RuntimeError("Could not fill planes before local power routing")
complete(board, signals=args.clocks,
signal_classes=frozenset({"CLOCK"}) if args.clocks else None)
if not pcbnew.ZONE_FILLER(board).Fill(board.Zones()):
raise RuntimeError("Could not refill planes after local power routing")
pcbnew.SaveBoard(str(args.output), board)
for suffix in (".kicad_pro", ".kicad_dru"):
source = args.input.with_suffix(suffix)
if source.exists():
shutil.copyfile(source, args.output.with_suffix(suffix))
print(f"Saved {args.output}")
if __name__ == "__main__":
main()
+284
View File
@@ -0,0 +1,284 @@
#!/usr/bin/env python3
"""Route the three buck converters before general signal autorouting.
The switching nodes and high-current input/output paths are deterministic and
locked. Freerouting must never be asked to invent these loops. Internal-plane
entry points use explicit via arrays; ordinary low-current power stitching is
left to stitch_plane_pads.py.
"""
from __future__ import annotations
import argparse
import shutil
from pathlib import Path
import pcbnew
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_INPUT = ROOT / "hardware" / "board_prerouted.kicad_pcb"
DEFAULT_OUTPUT = ROOT / "hardware" / "board_power_routed.kicad_pcb"
def mm(value: float) -> int:
return pcbnew.FromMM(value)
def point(x: float, y: float) -> pcbnew.VECTOR2I:
return pcbnew.VECTOR2I(mm(x), mm(y))
def pad(board: pcbnew.BOARD, reference: str, number: str) -> pcbnew.PAD:
footprint = board.FindFootprintByReference(reference)
if footprint is None:
raise RuntimeError(f"Missing footprint {reference}")
matches = [item for item in footprint.Pads() if item.GetNumber() == number]
if not matches:
raise RuntimeError(f"Missing pad {reference}.{number}")
return matches[0]
def xy(item: pcbnew.BOARD_ITEM) -> tuple[float, float]:
position = item.GetPosition()
return pcbnew.ToMM(position.x), pcbnew.ToMM(position.y)
def assert_pad_net(board: pcbnew.BOARD, reference: str, number: str,
expected: str) -> None:
actual = pad(board, reference, number).GetNetname()
if actual != expected:
raise RuntimeError(
f"{reference}.{number}: expected {expected}, found {actual or '<none>'}"
)
def add_track(board: pcbnew.BOARD, net_name: str,
start: tuple[float, float], end: tuple[float, float],
width: float, layer: int = pcbnew.F_Cu) -> None:
if start == end:
return
track = pcbnew.PCB_TRACK(board)
track.SetStart(point(*start))
track.SetEnd(point(*end))
track.SetWidth(mm(width))
track.SetLayer(layer)
track.SetNet(board.FindNet(net_name))
track.SetLocked(True)
board.Add(track)
def add_path(board: pcbnew.BOARD, net_name: str,
points: list[tuple[float, float]], width: float,
layer: int = pcbnew.F_Cu) -> None:
for start, end in zip(points, points[1:]):
add_track(board, net_name, start, end, width, layer)
def add_via(board: pcbnew.BOARD, net_name: str, x: float, y: float,
diameter: float = 0.70, drill: float = 0.35) -> None:
via = pcbnew.PCB_VIA(board)
via.SetPosition(point(x, y))
via.SetWidth(mm(diameter))
via.SetDrill(mm(drill))
via.SetLayerPair(pcbnew.F_Cu, pcbnew.B_Cu)
via.SetNet(board.FindNet(net_name))
via.SetLocked(True)
board.Add(via)
def route_core_buck(board: pcbnew.BOARD) -> None:
for reference, number, expected in (
("U13", "4", "SYS5"), ("U13", "9", "CORE_SW"),
("U13", "23", "CORE_COMP"), ("U13", "24", "CORE_SENSE_N"),
("L1", "1", "CORE_SW"), ("L1", "2", "VCC0V95"),
("C18", "1", "SYS5"), ("C22", "1", "VCC0V95"),
("R27", "2", "CORE_SENSE_P"), ("R28", "2", "CORE_SENSE_N"),
):
assert_pad_net(board, reference, number, expected)
# The exposed pad carries the converter return/thermal current. Four
# via-in-pad connections plus explicit perimeter-pin escapes replace the
# generic stitching that is intentionally disabled for U13.
for x in (99.70, 100.30):
for y in (92.40, 93.60):
add_via(board, "GND", x, y, 0.45, 0.225)
add_track(board, "GND", xy(pad(board, "U13", "3")), (98.10, 92.25), 0.22)
add_via(board, "GND", 98.10, 92.25, 0.45, 0.225)
add_track(board, "GND", xy(pad(board, "U13", "7")), (98.10, 94.25), 0.22)
add_via(board, "GND", 98.10, 94.25, 0.45, 0.225)
add_path(board, "GND", [xy(pad(board, "U13", "8")),
(98.675, 95.10), (98.10, 95.10)], 0.22)
add_via(board, "GND", 98.10, 95.10, 0.45, 0.225)
add_track(board, "GND", xy(pad(board, "U13", "19")), (101.65, 91.75), 0.22)
add_via(board, "GND", 101.65, 91.75, 0.45, 0.225)
add_path(board, "GND", [xy(pad(board, "U13", "20")),
(101.25, 91.10), (101.65, 91.10)], 0.22)
add_via(board, "GND", 101.65, 91.10, 0.45, 0.225)
add_path(board, "GND", [xy(pad(board, "U13", "22")),
(100.25, 92.40), (100.30, 92.40)], 0.22)
add_track(board, "GND", xy(pad(board, "U13", "14")), (101.65, 94.25), 0.22)
add_via(board, "GND", 101.65, 94.25, 0.45, 0.225)
add_path(board, "GND", [xy(pad(board, "U13", "13")),
(101.25, 95.10), (101.65, 95.10)], 0.22)
add_via(board, "GND", 101.65, 95.10, 0.45, 0.225)
# TPS6287B switching pins fan into a short common neck, then immediately
# enter L1. No other net is permitted through this high-dV/dt region.
for number in ("9", "10", "11", "12"):
x, y = xy(pad(board, "U13", number))
add_track(board, "CORE_SW", (x, y), (x, 95.25), 0.20)
add_track(board, "CORE_SW", (99.25, 95.25), (100.75, 95.25), 0.35)
add_track(board, "CORE_SW", (100.0, 95.25), xy(pad(board, "L1", "1")), 0.80)
# Expand immediately after L1 into the four 100-uF output capacitors.
l1_out = xy(pad(board, "L1", "2"))
add_track(board, "VCC0V95", l1_out, (100.0, 102.025), 1.20)
add_track(board, "VCC0V95", (95.5, 102.025), (106.0, 102.025), 1.00)
# A separate broad trunk and four-via entry feed the 0.95-V internal plane.
add_path(board, "VCC0V95",
[(106.0, 102.025), (108.2, 102.025), (108.2, 93.0)], 1.50)
for x in (107.8, 108.6):
for y in (92.6, 93.4):
add_via(board, "VCC0V95", x, y, 0.75, 0.38)
# Both VIN sides of U13 are locally decoupled. The right bank joins the
# left bank above the IC, away from the switching/output trunk.
add_track(board, "SYS5", xy(pad(board, "C18", "1")),
xy(pad(board, "C19", "1")), 0.60)
for number in ("4", "5", "6"):
source = xy(pad(board, "U13", number))
add_track(board, "SYS5", source, (97.175, source[1]), 0.25)
add_track(board, "SYS5", xy(pad(board, "C20", "1")),
xy(pad(board, "C21", "1")), 0.60)
for number in ("15", "16", "18"):
source = xy(pad(board, "U13", number))
add_track(board, "SYS5", source, (102.725, source[1]), 0.25)
# Feed the right input-capacitor bank from the existing B.Cu VIN trunk so
# the quiet compensation/sense region above U13 remains unobstructed.
add_track(board, "SYS5", xy(pad(board, "C20", "1")),
(102.725, 90.70), 0.60)
add_via(board, "SYS5", 102.725, 90.70, 0.70, 0.35)
add_path(board, "SYS5", [(102.725, 90.70), (110.00, 90.70),
(110.00, 97.20), (109.00, 97.20)],
0.80, pcbnew.B_Cu)
# Drop to B.Cu just below C19, bypassing the power-sequencer components
# that occupy the left side of the converter on F.Cu.
add_track(board, "SYS5", xy(pad(board, "C19", "1")),
(97.175, 95.60), 0.60)
add_via(board, "SYS5", 97.175, 95.60, 0.75, 0.38)
add_track(board, "SYS5", (97.175, 95.60), (97.175, 97.20),
0.80, pcbnew.B_Cu)
add_path(board, "SYS5", [(97.175, 97.20), (109.00, 97.20),
(109.00, 108.50), (92.60, 108.50)],
1.20, pcbnew.B_Cu)
for x in (92.2, 93.0):
for y in (108.1, 108.9):
add_via(board, "SYS5", x, y, 0.75, 0.38)
# Compensation components form a literal series chain. Keep it on F.Cu
# and out of the switch-node field.
add_track(board, "CORE_COMP_RC", xy(pad(board, "C16", "1")),
xy(pad(board, "R29", "2")), 0.15)
add_track(board, "CORE_COMP", xy(pad(board, "R29", "1")),
xy(pad(board, "C17", "1")), 0.15)
add_path(board, "CORE_COMP",
[xy(pad(board, "C17", "1")), (99.52, 90.20),
(99.75, 90.20),
xy(pad(board, "U13", "23"))], 0.15)
# Route the positive/negative remote-sense pair together on L3_SIG. The
# pair stays north of the converter and never approaches CORE_SW.
sense = (
("CORE_SENSE_P", "1", "R27", "2", 98.40, 90.40, 87.60),
("CORE_SENSE_N", "24", "R28", "2", 99.10, 90.70, 89.70),
)
for (net_name, u13_pin, resistor, resistor_pin,
start_x, start_y, lane_y) in sense:
start = xy(pad(board, "U13", u13_pin))
start_via = (start_x, start_y)
end = xy(pad(board, resistor, resistor_pin))
end_via = (102.90, end[1])
add_path(board, net_name, [start, (start[0], start_y), start_via], 0.12)
add_via(board, net_name, *start_via, 0.45, 0.225)
add_path(board, net_name,
[start_via, (start_x, lane_y), (102.90, lane_y), end_via],
0.12, pcbnew.In2_Cu)
add_via(board, net_name, *end_via, 0.45, 0.225)
add_track(board, net_name, end_via, end, 0.12)
def route_small_buck(board: pcbnew.BOARD, *, regulator: str, inductor: str,
input_caps: tuple[str, str], output_caps: tuple[str, str],
sw_net: str, out_net: str, input_x: float,
plane_via_x: float) -> None:
for reference, number, expected in (
(regulator, "3", "SYS5"), (regulator, "7", sw_net),
(inductor, "1", sw_net), (inductor, "2", out_net),
(input_caps[0], "1", "SYS5"), (input_caps[1], "1", "SYS5"),
(output_caps[0], "1", out_net), (output_caps[1], "1", out_net),
):
assert_pad_net(board, reference, number, expected)
# Input capacitors, VIN pin, and the SYS5 plane entry form one compact path.
upper_in = xy(pad(board, input_caps[0], "1"))
lower_in = xy(pad(board, input_caps[1], "1"))
add_track(board, "SYS5", upper_in, lower_in, 0.45)
regulator_in = xy(pad(board, regulator, "3"))
diagonal_start_x = lower_in[0] + abs(lower_in[1] - regulator_in[1])
add_path(board, "SYS5", [regulator_in,
(diagonal_start_x, regulator_in[1]), lower_in], 0.30)
add_track(board, "SYS5", lower_in, (input_x, 107.60), 0.70)
for x in (input_x - 0.35, input_x + 0.35):
for y in (107.25, 107.95):
add_via(board, "SYS5", x, y, 0.65, 0.33)
# Keep the switching node to the literal pin-to-inductor span.
add_track(board, sw_net, xy(pad(board, regulator, "7")),
xy(pad(board, inductor, "1")), 0.50)
# Join the inductor, output capacitors, regulator output pins and a
# plane entry installed by stitch_plane_pads.py. These spans stay on F.Cu.
coil_out = xy(pad(board, inductor, "2"))
cap_a = xy(pad(board, output_caps[0], "1"))
cap_b = xy(pad(board, output_caps[1], "1"))
add_path(board, out_net, [coil_out, (plane_via_x, 97.0),
(plane_via_x, cap_a[1]), cap_a], 0.55)
add_path(board, out_net, [(plane_via_x, 97.0),
(plane_via_x, cap_b[1]), cap_b], 0.55)
reg_out_a = xy(pad(board, regulator, "8"))
reg_out_b = xy(pad(board, regulator, "9"))
add_track(board, out_net, reg_out_a, reg_out_b, 0.25)
add_path(board, out_net,
[reg_out_b, (plane_via_x, reg_out_b[1]), (plane_via_x, 97.0)], 0.30)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("input", nargs="?", type=Path, default=DEFAULT_INPUT)
parser.add_argument("output", nargs="?", type=Path, default=DEFAULT_OUTPUT)
args = parser.parse_args()
board = pcbnew.LoadBoard(str(args.input))
route_core_buck(board)
route_small_buck(
board, regulator="U14", inductor="L2", input_caps=("C26", "C27"),
output_caps=("C28", "C29"), sw_net="BUCK1V8_SW", out_net="VCC1V8",
input_x=112.98, plane_via_x=120.105,
)
route_small_buck(
board, regulator="U15", inductor="L3", input_caps=("C30", "C31"),
output_caps=("C32", "C33"), sw_net="BUCK3V3_SW", out_net="VCC3V3",
input_x=127.98, plane_via_x=135.105,
)
if not pcbnew.ZONE_FILLER(board).Fill(board.Zones()):
raise RuntimeError("Zone fill failed after power-stage routing")
pcbnew.SaveBoard(str(args.output), board)
for suffix in (".kicad_pro", ".kicad_dru"):
source = args.input.with_suffix(suffix)
if source.exists():
shutil.copyfile(source, args.output.with_suffix(suffix))
print(f"Saved {args.output}")
if __name__ == "__main__":
main()
+164 -47
View File
@@ -5,6 +5,10 @@ This deliberately does not replace power-stage routing. It only installs
short pad-to-via connections where a filled, same-net plane is physically short pad-to-via connections where a filled, same-net plane is physically
present below the candidate via. High-current converter loops and plane present below the candidate via. High-current converter loops and plane
bridges are handled separately. bridges are handled separately.
It runs *after* route_power_stage.py: a candidate via is only checked against
copper that already exists, so any stage that lays down wide trunks has to go
first or its copper is invisible here.
""" """
from __future__ import annotations from __future__ import annotations
@@ -18,7 +22,7 @@ import pcbnew
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
DEFAULT_INPUT = ROOT / "hardware" / "board_prerouted.kicad_pcb" DEFAULT_INPUT = ROOT / "hardware" / "board_power_routed.kicad_pcb"
DEFAULT_OUTPUT = ROOT / "hardware" / "board_plane_stitched.kicad_pcb" DEFAULT_OUTPUT = ROOT / "hardware" / "board_plane_stitched.kicad_pcb"
PLANE_NETS = { PLANE_NETS = {
"GND", "SYS5", "VCC0V95", "VCC1V8", "VCC3V3", "VCC3V3_AUX", "GND", "SYS5", "VCC0V95", "VCC1V8", "VCC3V3", "VCC3V3_AUX",
@@ -38,7 +42,7 @@ def distance_mm(a: pcbnew.VECTOR2I, b: pcbnew.VECTOR2I) -> float:
def add_track(board: pcbnew.BOARD, pad: pcbnew.PAD, def add_track(board: pcbnew.BOARD, pad: pcbnew.PAD,
end: pcbnew.VECTOR2I, width: float) -> None: end: pcbnew.VECTOR2I, width: float) -> pcbnew.PCB_TRACK:
track = pcbnew.PCB_TRACK(board) track = pcbnew.PCB_TRACK(board)
track.SetStart(pad.GetPosition()) track.SetStart(pad.GetPosition())
track.SetEnd(end) track.SetEnd(end)
@@ -47,6 +51,23 @@ def add_track(board: pcbnew.BOARD, pad: pcbnew.PAD,
track.SetNet(pad.GetNet()) track.SetNet(pad.GetNet())
track.SetLocked(True) track.SetLocked(True)
board.Add(track) board.Add(track)
return track
def already_connected(pad: pcbnew.PAD, tracks: list[pcbnew.PCB_TRACK],
vias: list[pcbnew.PCB_VIA]) -> bool:
"""Report whether an earlier stage already attached copper to this pad."""
net = pad.GetNetname()
box = pad.GetBoundingBox()
def inside(position: pcbnew.VECTOR2I) -> bool:
return (box.GetX() <= position.x <= box.GetRight() and
box.GetY() <= position.y <= box.GetBottom())
for track in tracks:
if track.GetNetname() == net and (inside(track.GetStart()) or inside(track.GetEnd())):
return True
return any(via.GetNetname() == net and inside(via.GetPosition()) for via in vias)
def add_via(board: pcbnew.BOARD, pad: pcbnew.PAD, def add_via(board: pcbnew.BOARD, pad: pcbnew.PAD,
@@ -68,6 +89,66 @@ def plane_at(zones_by_net: dict[str, list[pcbnew.ZONE]], net: str,
for zone in zones_by_net.get(net, ())) for zone in zones_by_net.get(net, ()))
def inside_temporary_keepout(position: pcbnew.VECTOR2I,
keepouts: list[pcbnew.ZONE]) -> bool:
"""Reject a through-via whose copper can touch an RF/USB rule area."""
radius = VIA_DIAMETER / 2 + CLEARANCE
probes = [(0.0, 0.0)]
for index in range(8):
angle = index * math.pi / 4
probes.append((radius * math.cos(angle), radius * math.sin(angle)))
for dx, dy in probes:
probe = pcbnew.VECTOR2I(position.x + mm(dx), position.y + mm(dy))
if any(zone.Outline().Contains(probe) for zone in keepouts):
return True
return False
def segment_distance_mm(point: pcbnew.VECTOR2I, start: pcbnew.VECTOR2I,
end: pcbnew.VECTOR2I) -> float:
px, py = pcbnew.ToMM(point.x), pcbnew.ToMM(point.y)
ax, ay = pcbnew.ToMM(start.x), pcbnew.ToMM(start.y)
bx, by = pcbnew.ToMM(end.x), pcbnew.ToMM(end.y)
dx, dy = bx - ax, by - ay
length = dx * dx + dy * dy
if length == 0.0:
return math.hypot(px - ax, py - ay)
t = max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / length))
return math.hypot(px - (ax + t * dx), py - (ay + t * dy))
def clear_of_tracks(position: pcbnew.VECTOR2I, net: str,
tracks: list[pcbnew.PCB_TRACK], extra: float = 0.0) -> bool:
"""Keep a via barrel clear of copper installed by the routing stages."""
for track in tracks:
if track.GetNetname() == net or isinstance(track, pcbnew.PCB_VIA):
continue
limit = (VIA_DIAMETER / 2 + extra + pcbnew.ToMM(track.GetWidth()) / 2
+ CLEARANCE + 0.03)
if segment_distance_mm(position, track.GetStart(), track.GetEnd()) < limit:
return False
return True
def clear_track_of_tracks(start: pcbnew.VECTOR2I, end: pcbnew.VECTOR2I, net: str,
tracks: list[pcbnew.PCB_TRACK], layer: int,
width: float) -> bool:
for track in tracks:
if track.GetNetname() == net or isinstance(track, pcbnew.PCB_VIA):
continue
if track.GetLayer() != layer:
continue
limit = width / 2 + pcbnew.ToMM(track.GetWidth()) / 2 + CLEARANCE + 0.03
for step in range(0, 13):
sample = pcbnew.VECTOR2I(
round(start.x + (end.x - start.x) * step / 12),
round(start.y + (end.y - start.y) * step / 12),
)
if segment_distance_mm(sample, track.GetStart(), track.GetEnd()) < limit:
return False
return True
def clear_of_other_copper(position: pcbnew.VECTOR2I, net: str, def clear_of_other_copper(position: pcbnew.VECTOR2I, net: str,
pads: list[pcbnew.PAD], vias: list[pcbnew.PCB_VIA]) -> bool: pads: list[pcbnew.PAD], vias: list[pcbnew.PCB_VIA]) -> bool:
x, y = pcbnew.ToMM(position.x), pcbnew.ToMM(position.y) x, y = pcbnew.ToMM(position.x), pcbnew.ToMM(position.y)
@@ -116,19 +197,93 @@ def candidates(pad: pcbnew.PAD, footprint: pcbnew.FOOTPRINT) -> list[pcbnew.VECT
away = 0.0 away = 0.0
radius = max(pcbnew.ToMM(pad.GetSizeX()), pcbnew.ToMM(pad.GetSizeY())) / 2 radius = max(pcbnew.ToMM(pad.GetSizeX()), pcbnew.ToMM(pad.GetSizeY())) / 2
radius += VIA_DIAMETER / 2 + CLEARANCE + 0.08 radius += VIA_DIAMETER / 2 + CLEARANCE + 0.08
angles = [away] # Freerouting treats fixed non-45-degree traces as invalid geometry. Use
for delta_deg in (30, -30, 60, -60, 90, -90, 120, -120, 150, -150, 180): # only global orthogonal/45-degree directions, ordered by how closely they
angles.append(away + math.radians(delta_deg)) # point away from the footprint centre.
directions = [(math.cos(math.radians(angle)), math.sin(math.radians(angle)))
for angle in range(0, 360, 45)]
directions.sort(key=lambda direction: -(
direction[0] * math.cos(away) + direction[1] * math.sin(away)
))
result = [] result = []
for scale in (1.0, 1.35, 1.75): for scale in (1.0, 1.35, 1.75):
for angle in angles: for dx, dy in directions:
result.append(pcbnew.VECTOR2I( result.append(pcbnew.VECTOR2I(
pad_pos.x + mm(radius * scale * math.cos(angle)), pad_pos.x + mm(radius * scale * dx),
pad_pos.y + mm(radius * scale * math.sin(angle)), pad_pos.y + mm(radius * scale * dy),
)) ))
return result return result
def stitch(board: pcbnew.BOARD) -> tuple[dict[str, int], dict[str, int]]:
"""Connect every reachable SMD power pad to its plane on the given board.
Callable twice: once before signal routing, so the planes already have
real connection points, and again from finalize_routing.py once the RF and
USB corridors are no longer reserved by temporary rule areas. Pads that
are already stitched are skipped because the candidate location is then
occupied by the earlier via.
"""
zones_by_net: dict[str, list[pcbnew.ZONE]] = {}
for zone in board.Zones():
if not zone.GetIsRuleArea() and zone.GetNetname() in PLANE_NETS:
zones_by_net.setdefault(zone.GetNetname(), []).append(zone)
temporary_keepouts = [zone for zone in board.Zones()
if zone.GetIsRuleArea() and zone.GetZoneName().startswith("TEMP_")]
all_pads = [pad for fp in board.GetFootprints() for pad in fp.Pads()]
all_vias = [item for item in board.GetTracks() if isinstance(item, pcbnew.PCB_VIA)]
all_tracks = [item for item in board.GetTracks() if not isinstance(item, pcbnew.PCB_VIA)]
connected: dict[str, int] = {}
skipped: dict[str, int] = {}
for fp in board.GetFootprints():
# U1 already has deterministic dogbones. J2 is an edge-launch part;
# its ground lands are completed by finalize_routing.py's RF ground
# zone/fence, not by ordinary tracks that would run along Edge.Cuts.
# U13 ground/thermal vias are part of the deterministic high-current
# converter geometry in route_power_stage.py. Generic radial dogbones
# here can intrude into its SW and VIN paths.
if fp.GetReference() in {"U1", "U13", "J2"}:
continue
for pad in fp.Pads():
net = pad.GetNetname()
if net not in PLANE_NETS or pad.GetAttribute() != pcbnew.PAD_ATTRIB_SMD:
continue
if already_connected(pad, all_tracks, all_vias):
continue
chosen = None
pad_min = min(pcbnew.ToMM(pad.GetSizeX()), pcbnew.ToMM(pad.GetSizeY()))
width = max(0.15, min(0.30, pad_min * 0.65))
for candidate in candidates(pad, fp):
x, y = pcbnew.ToMM(candidate.x), pcbnew.ToMM(candidate.y)
if not (20.5 < x < 174.5 and 20.5 < y < 109.5):
continue
if not plane_at(zones_by_net, net, candidate):
continue
if inside_temporary_keepout(candidate, temporary_keepouts):
continue
if not clear_of_other_copper(candidate, net, all_pads, all_vias):
continue
if not clear_track_path(pad.GetPosition(), candidate, net, pad, all_pads, width):
continue
if not clear_of_tracks(candidate, net, all_tracks):
continue
if not clear_track_of_tracks(pad.GetPosition(), candidate, net,
all_tracks, pad.GetLayer(), width):
continue
chosen = candidate
break
if chosen is None:
skipped[net] = skipped.get(net, 0) + 1
continue
all_tracks.append(add_track(board, pad, chosen, width))
all_vias.append(add_via(board, pad, chosen))
connected[net] = connected.get(net, 0) + 1
return connected, skipped
def main() -> None: def main() -> None:
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("input", nargs="?", type=Path, default=DEFAULT_INPUT) parser.add_argument("input", nargs="?", type=Path, default=DEFAULT_INPUT)
@@ -138,45 +293,7 @@ def main() -> None:
board = pcbnew.LoadBoard(str(args.input)) board = pcbnew.LoadBoard(str(args.input))
if not pcbnew.ZONE_FILLER(board).Fill(board.Zones()): if not pcbnew.ZONE_FILLER(board).Fill(board.Zones()):
raise RuntimeError("Could not fill planes before stitching") raise RuntimeError("Could not fill planes before stitching")
connected, skipped = stitch(board)
zones_by_net: dict[str, list[pcbnew.ZONE]] = {}
for zone in board.Zones():
if not zone.GetIsRuleArea() and zone.GetNetname() in PLANE_NETS:
zones_by_net.setdefault(zone.GetNetname(), []).append(zone)
all_pads = [pad for fp in board.GetFootprints() for pad in fp.Pads()]
all_vias = [item for item in board.GetTracks() if isinstance(item, pcbnew.PCB_VIA)]
connected: dict[str, int] = {}
skipped: dict[str, int] = {}
for fp in board.GetFootprints():
if fp.GetReference() == "U1":
continue
for pad in fp.Pads():
net = pad.GetNetname()
if net not in PLANE_NETS or pad.GetAttribute() != pcbnew.PAD_ATTRIB_SMD:
continue
chosen = None
pad_min = min(pcbnew.ToMM(pad.GetSizeX()), pcbnew.ToMM(pad.GetSizeY()))
width = max(0.15, min(0.30, pad_min * 0.65))
for candidate in candidates(pad, fp):
x, y = pcbnew.ToMM(candidate.x), pcbnew.ToMM(candidate.y)
if not (20.5 < x < 174.5 and 20.5 < y < 109.5):
continue
if not plane_at(zones_by_net, net, candidate):
continue
if not clear_of_other_copper(candidate, net, all_pads, all_vias):
continue
if not clear_track_path(pad.GetPosition(), candidate, net, pad, all_pads, width):
continue
chosen = candidate
break
if chosen is None:
skipped[net] = skipped.get(net, 0) + 1
continue
add_track(board, pad, chosen, width)
all_vias.append(add_via(board, pad, chosen))
connected[net] = connected.get(net, 0) + 1
if not pcbnew.ZONE_FILLER(board).Fill(board.Zones()): if not pcbnew.ZONE_FILLER(board).Fill(board.Zones()):
raise RuntimeError("Could not refill planes after stitching") raise RuntimeError("Could not refill planes after stitching")