Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1218c33aa4 | |||
| 791ca5c9fa | |||
| 4fa38e35bd | |||
| 1ea2bc64ac | |||
| 95609a6b6a | |||
| af7c43b2a8 |
+15
-2
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 |
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
File diff suppressed because it is too large
Load Diff
+4760
-4198
File diff suppressed because it is too large
Load Diff
+288
-3
@@ -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)))
|
||||||
|
|||||||
@@ -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
@@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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)`.
|
||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
|
+1166
-1139
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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 /
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
|
@@ -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
@@ -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 |
|
||||||
|
|||||||
@@ -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
@@ -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))
|
||||||
|
|||||||
@@ -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
@@ -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")
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user