Initial Ti375 GNSS IMU board design and documentation
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
# Üretim, doğrulama ve routing akışı
|
||||
|
||||
## Gereksinimler
|
||||
|
||||
- KiCad 10.x ve `kicad-cli`.
|
||||
- KiCad ile aynı sürümden `/usr/bin/python3` ve `pcbnew` modülü.
|
||||
- Python paketleri: `pip install -r requirements.txt`.
|
||||
- Yoğun sinyal routing aşaması için isteğe bağlı Freerouting 2.3.x.
|
||||
|
||||
`pcbnew` kullanan scriptler sistem Python'u ile çalıştırılmalıdır. Normal
|
||||
Python ortamına yalnızca `pip install pcbnew` yapmak KiCad ABI uyumluluğunu
|
||||
garanti etmez.
|
||||
|
||||
## Dosya üretim sırası
|
||||
|
||||
Repository kökünden:
|
||||
|
||||
```bash
|
||||
python3 scripts/generate_footprints.py
|
||||
python3 scripts/generate_kicad.py
|
||||
python3 scripts/generate_design_tables.py
|
||||
/usr/bin/python3 scripts/generate_pcb.py
|
||||
```
|
||||
|
||||
Bu sıralama:
|
||||
|
||||
1. Özel footprintleri üretir.
|
||||
2. `design_data.py` üzerinden şema hiyerarşisini ve BOM'u üretir.
|
||||
3. Efinity C529 paket verisine karşı pin tablolarını doğrular ve üretir.
|
||||
4. 8 katmanlı stackup, kurallar, zone'lar ve placement ile PCB'yi üretir.
|
||||
|
||||
`design_data.py` çalıştırıldığında dahili bağlantı assertion'ları da yürütülür:
|
||||
|
||||
```bash
|
||||
python3 scripts/design_data.py
|
||||
```
|
||||
|
||||
## Şema ve placement doğrulaması
|
||||
|
||||
```bash
|
||||
kicad-cli sch erc --severity-all \
|
||||
-o outputs/erc_final_reset.rpt hardware/board.kicad_sch
|
||||
|
||||
kicad-cli pcb drc --severity-all \
|
||||
-o outputs/drc_final_placement.rpt hardware/board.kicad_pcb
|
||||
```
|
||||
|
||||
Unrouted placement aşamasında unconnected-pad ve isolated-zone kayıtları
|
||||
beklenir. Clearance, short, courtyard, solder-mask, edge ve footprint hataları
|
||||
beklenmez.
|
||||
|
||||
## Routing sırası
|
||||
|
||||
### 1. BGA escape
|
||||
|
||||
```bash
|
||||
/usr/bin/python3 scripts/preroute_bga.py \
|
||||
hardware/board.kicad_pcb hardware/board_prerouted.kicad_pcb
|
||||
```
|
||||
|
||||
Ti375'in bağlı ball'larına kilitli 0.8 mm pitch dogbone/via escape ekler.
|
||||
|
||||
### 2. Yerel plane bağlantıları
|
||||
|
||||
```bash
|
||||
/usr/bin/python3 scripts/stitch_plane_pads.py \
|
||||
hardware/board_prerouted.kicad_pcb \
|
||||
hardware/board_plane_stitched.kicad_pcb
|
||||
```
|
||||
|
||||
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
|
||||
yerini tutmaz.
|
||||
|
||||
### 3. Güç katı
|
||||
|
||||
KiCad içinde önce aşağıdaki hatlar elle route edilmelidir:
|
||||
|
||||
- TPS6287B15 giriş kapasitörü, SW, indüktör, çıkış kapasitörü ve remote-sense.
|
||||
- TPS62867 1.8 V ve 3.3 V converter loopları.
|
||||
- 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,
|
||||
Kelvin bağlantılı götürülmelidir. Autorouter bu kararları vermez.
|
||||
|
||||
### 4. Freerouting için sınıf bazlı DSN
|
||||
|
||||
```bash
|
||||
/usr/bin/python3 scripts/export_routing_stage.py \
|
||||
hardware/board_plane_stitched.kicad_pcb outputs/signal_stage.dsn \
|
||||
--classes "Default;BGA_ESCAPE;CLOCK" \
|
||||
--preview outputs/signal_stage_preview.kicad_pcb
|
||||
|
||||
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;
|
||||
koordinatlara, track genişliklerine, via veya keepoutlara dokunmaz. Freerouting
|
||||
arayüzünde düzeltilmiş DSN açılır, en fazla 8 thread kullanılır ve sonuç `.ses`
|
||||
olarak kaydedilir.
|
||||
|
||||
RF, USB, güç converter loopları ve hassas clock kararları otomatik sonuca
|
||||
bırakılmamalıdır. `to route` sayısının uzun süre düşmemesi halinde aynı işlemi
|
||||
saatlerce sürdürmek yerine placement/escape/keepout çatışması incelenmelidir.
|
||||
|
||||
### 5. Kritik hatları kur ve session'ı sonlandır
|
||||
|
||||
```bash
|
||||
/usr/bin/python3 scripts/finalize_routing.py \
|
||||
outputs/signal_stage.ses \
|
||||
--output hardware/board_routed.kicad_pcb
|
||||
```
|
||||
|
||||
Bu işlem Freerouter'ın RF ve USB üzerindeki copperını kaldırır, denetlenmiş
|
||||
RF/USB geometrisini ekler, RF GND via fence'i kurar ve zone'ları doldurur.
|
||||
|
||||
### 6. Final kontroller
|
||||
|
||||
```bash
|
||||
kicad-cli pcb drc --severity-all --schematic-parity --refill-zones \
|
||||
--save-board -o outputs/drc_final_routed.rpt \
|
||||
hardware/board_routed.kicad_pcb
|
||||
```
|
||||
|
||||
Final kartta unconnected item, short, clearance, isolated copper veya
|
||||
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.
|
||||
|
||||
## Script özeti
|
||||
|
||||
| Script | Görev |
|
||||
|---|---|
|
||||
| `design_data.py` | Tek kaynak komponent/net verisi ve bağlantı assertion'ları |
|
||||
| `generate_footprints.py` | Üretici land-patternlerinden özel footprint üretimi |
|
||||
| `generate_kicad.py` | Şema, sembol ve BOM üretimi |
|
||||
| `generate_design_tables.py` | Ti375 pinout/constraint ve MAX2771 tablo üretimi |
|
||||
| `generate_pcb.py` | Stackup, rule, zone ve deterministic placement üretimi |
|
||||
| `preroute_bga.py` | Kilitli Ti375 dogbone escape |
|
||||
| `stitch_plane_pads.py` | Yerel pad-to-plane bağlantıları |
|
||||
| `export_routing_stage.py` | Seçili net-class için DSN üretimi |
|
||||
| `prepare_freerouting_dsn.py` | Freerouting 2.3 clearance düzeltmesi |
|
||||
| `finalize_routing.py` | SES importu ve denetlenmiş RF/USB geometrisi |
|
||||
@@ -0,0 +1,794 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Single-source component and connectivity data for the GNSS/IMU FPGA PCB.
|
||||
|
||||
Generated KiCad, BOM, pinout, and assembly data import this module. Ti375 ball
|
||||
data comes from the Efinity 2025.2 C529 resource DB, never a hand transcription.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Pin:
|
||||
number: str
|
||||
name: str
|
||||
net: str | None
|
||||
etype: str = "passive"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Component:
|
||||
ref: str
|
||||
value: str
|
||||
mpn: str
|
||||
manufacturer: str
|
||||
footprint: str
|
||||
pins: list[Pin]
|
||||
description: str = ""
|
||||
assembly: str = "SMT"
|
||||
dnp: bool = False
|
||||
fields: dict[str, str] = field(default_factory=dict)
|
||||
|
||||
|
||||
def p(number: str | int, name: str, net: str | None, etype: str = "passive") -> Pin:
|
||||
return Pin(str(number), name, net, etype)
|
||||
|
||||
|
||||
components: list[Component] = []
|
||||
_refs = {"R": 0, "C": 0, "L": 0, "FB": 0, "TP": 0}
|
||||
|
||||
|
||||
def add(component: Component) -> Component:
|
||||
assert not any(c.ref == component.ref for c in components), component.ref
|
||||
components.append(component)
|
||||
return component
|
||||
|
||||
|
||||
def two_pin(prefix: str, value: str, mpn: str, manufacturer: str,
|
||||
footprint: str, net1: str, net2: str, description: str,
|
||||
*, dnp: bool = False, ref: str | None = None) -> Component:
|
||||
if ref is None:
|
||||
_refs[prefix] += 1
|
||||
ref = f"{prefix}{_refs[prefix]}"
|
||||
return add(Component(ref, value, mpn, manufacturer, footprint,
|
||||
[p(1, "1", net1), p(2, "2", net2)],
|
||||
description, dnp=dnp))
|
||||
|
||||
|
||||
def r(value: str, net1: str, net2: str, *, mpn: str = "",
|
||||
description: str = "Resistor", dnp: bool = False) -> Component:
|
||||
values = {
|
||||
"0R": "RC0402JR-070RL", "22R": "RC0402FR-0722RL",
|
||||
"27R": "RC0402FR-0727RL", "33R": "RC0402FR-0733RL",
|
||||
"825R": "RC0402FR-07825RL", "1K": "RC0402FR-071KL", "1.24K": "RC0402FR-071K24L",
|
||||
"2.1K": "RC0402FR-072K1L", "2.2K": "RC0402FR-072K2L",
|
||||
"4.7K": "RC0402FR-074K7L", "10K": "RC0402FR-0710KL",
|
||||
"12K": "RC0402FR-0712KL", "15K": "RC0402FR-0715KL",
|
||||
"22.1K": "RC0402FR-0722K1L", "100K": "RC0402FR-07100KL",
|
||||
"133K": "RC0402FR-07133KL", "205K": "RC0402FR-07205KL",
|
||||
"232K": "RC0402FR-07232KL", "383K": "RC0402FR-07383KL",
|
||||
"442K": "RC0402FR-07442KL", "900K": "RC0402FR-07909KL",
|
||||
"1M": "RC0402FR-071ML",
|
||||
}
|
||||
return two_pin("R", value, mpn or values.get(value, ""), "Yageo",
|
||||
"Resistor_SMD:R_0402_1005Metric", net1, net2, description,
|
||||
dnp=dnp)
|
||||
|
||||
|
||||
def c(value: str, net1: str, net2: str, *, mpn: str = "",
|
||||
footprint: str = "Capacitor_SMD:C_0402_1005Metric",
|
||||
description: str = "MLCC", dnp: bool = False) -> Component:
|
||||
values = {
|
||||
"10pF": "GRM1555C1H100JA01D", "15pF": "GRM1555C1H150JA01D",
|
||||
"27pF": "600L270JT200T", "750pF": "GRM1555C1H751JA01D",
|
||||
"1.8nF": "GRM155R71H182KA01D", "4.7nF": "GRM155R71H472KA01D",
|
||||
"10nF": "GRM155R71H103KA88D", "100nF": "GRM155R71A104KA01D",
|
||||
"470nF": "GCM155C71A474KE36D", "1uF": "GRM155R60J105KE19D",
|
||||
"10uF": "GRM188R60J106ME47D", "22uF": "GRM21BZ70J226ME44L",
|
||||
"47uF": "GRM32ER70J476ME19L", "100uF": "GRT32EC70J107ME13L",
|
||||
}
|
||||
return two_pin("C", value, mpn or values.get(value, ""), "Murata",
|
||||
footprint, net1, net2, description, dnp=dnp)
|
||||
|
||||
|
||||
def tp(net: str, label: str) -> Component:
|
||||
_refs["TP"] += 1
|
||||
return add(Component(f"TP{_refs['TP']}", label, "", "",
|
||||
"TestPoint:TestPoint_Pad_D1.0mm",
|
||||
[p(1, label, net)], "Local labelled SMT bring-up test pad", "SMT"))
|
||||
|
||||
|
||||
def add_ic(ref: str, value: str, mpn: str, manufacturer: str, footprint: str,
|
||||
pins: list[Pin], description: str) -> Component:
|
||||
return add(Component(ref, value, mpn, manufacturer, footprint, pins, description))
|
||||
|
||||
|
||||
# Ti375 C529: represent every package ball exactly once.
|
||||
pinout = {row["ball"]: row for row in csv.DictReader(
|
||||
(ROOT / "docs" / "ti375_pinout.csv").open(newline=""))}
|
||||
package = list(csv.DictReader((ROOT / "docs" / "ti375_c529_package.csv").open(newline="")))
|
||||
|
||||
|
||||
def fpga_net(row: dict[str, str]) -> str | None:
|
||||
ball = row["ball"]
|
||||
if ball in pinout:
|
||||
return pinout[ball]["signal"]
|
||||
name = row["official_pad_name"]
|
||||
if name in ("GND", "VQPS"):
|
||||
return "GND"
|
||||
if name == "VCC" or name.startswith("VCCA_") or name == "VDD_SOC":
|
||||
return "VCC0V95"
|
||||
if name == "VCCAUX":
|
||||
return "VCC1V8"
|
||||
vccio = {
|
||||
"VCCIO4C": "VCC1V8", "VCCIO4A_4B": "VCC1V8", "VCCIO4D": "VCC1V8",
|
||||
"VCCIO2A": "VCC1V8", "VCCIO2B": "VCC1V8", "VCCIO2C": "VCC1V8",
|
||||
"VCCIO2D": "VCC1V8", "VCCIO2E": "VCC1V8",
|
||||
"VCCIO33_BR0": "VCC3V0_DIG", "VCCIO33_TR0": "VCC3V0_DIG",
|
||||
"VCCIO33_TR1": "VCC3V3", "VCCIO33_TR2": "VCC3V3",
|
||||
"VCCIO33_BR3_BR4": "VCC3V3", "VCCIO33_TL1_TL5": "VCC3V3",
|
||||
"VCCIO33_BL2_BL3": "VCC3V3",
|
||||
}
|
||||
if name in vccio:
|
||||
return vccio[name]
|
||||
if name.startswith("REF_RES_"):
|
||||
return name
|
||||
# JTAG_VCCIO_SEL floats for 3.3-V BR3/BR4. Official NC, unused DDR
|
||||
# signals, and unused DDR rails remain open per Efinix's unused table.
|
||||
return None
|
||||
|
||||
|
||||
fpga_pins = []
|
||||
for row in package:
|
||||
etype = {"power": "power_in", "input": "input", "output": "output"}.get(
|
||||
row["direction"], "bidirectional")
|
||||
fpga_pins.append(p(row["ball"], row["official_pad_name"], fpga_net(row), etype))
|
||||
add_ic("U1", "Ti375C529C3", "Ti375C529C3", "Efinix",
|
||||
"Package_BGA:BGA-529_19x19mm_Layout23x23_P0.8mm", fpga_pins,
|
||||
"Titanium Ti375 FPGA, commercial standard-power 0.95-V option, C529")
|
||||
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")
|
||||
|
||||
|
||||
# USB-C and sink role detection.
|
||||
add_ic("J1", "USB-C USB2 sink", "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", "USB_CC1"),
|
||||
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("B1", "GND", "GND"), p("B4", "VBUS", "VBUS_RAW"), p("B5", "CC2", "USB_CC2"),
|
||||
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")],
|
||||
"USB 2.0 Type-C receptacle, UFP/sink")
|
||||
add_ic("U5", "TUSB320LAI", "TUSB320LAIRSW(R)", "Texas Instruments",
|
||||
"Package_DFN_QFN:Texas_X2QFN-12_1.6x1.6mm_P0.4mm", [
|
||||
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(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; integrated Rd on CC1/CC2")
|
||||
r("900K", "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_OUT2", description="TUSB320 open-drain OUT2 pull-up")
|
||||
c("100nF", "VCC3V3_AUX", "GND", description="TUSB320 local bypass")
|
||||
add_ic("U6", "TPD2EUSB30A", "TPD2EUSB30ADRTR", "Texas Instruments",
|
||||
"Package_TO_SOT_SMD:Texas_DRT-3", [p(1, "IO1", "USB_D_P"), p(2, "GND", "GND"), p(3, "IO2", "USB_D_N")],
|
||||
"Low-capacitance USB 2.0 ESD protection")
|
||||
c("4.7nF", "USB_SHIELD", "GND", description="USB shield HF coupling")
|
||||
r("1M", "USB_SHIELD", "GND", description="USB shield bleed")
|
||||
r("0R", "USB_SHIELD", "GND", description="Optional direct shield bond", dnp=True)
|
||||
|
||||
add_ic("J3", "EXT_5V", "1935161", "Phoenix Contact",
|
||||
"TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-2_1x02_P5.00mm_Horizontal", [
|
||||
p(1, "EXT_5V", "EXT5_RAW", "power_in"), p(2, "GND", "GND", "power_in")],
|
||||
"Clearly labelled external regulated 5-V input")
|
||||
components[-1].assembly = "THT"
|
||||
|
||||
|
||||
def add_efuse(ref: str, input_net: str, output_net: str, ilim: str) -> None:
|
||||
add_ic(ref, "TPS259470A", "TPS259470ARPWR", "Texas Instruments",
|
||||
"board:Texas_RPW0010A_QFN10", [
|
||||
p(1, "EN/UVLO", f"{ref}_ENUV", "input"), p(2, "OVLO", f"{ref}_OV", "input"),
|
||||
p(3, "AUXOFF", None, "open_collector"), p(4, "FLT", f"{ref}_FLT", "open_collector"),
|
||||
p(5, "IN", input_net, "power_in"), p(6, "OUT", output_net, "power_out"),
|
||||
p(7, "DVDT", f"{ref}_DVDT", "output"), p(8, "GND", "GND", "power_in"),
|
||||
p(9, "ILM", f"{ref}_ILM", "output"), p(10, "ITIMER", None)],
|
||||
"True-reverse-blocking eFuse; UV/OV/current/inrush protection")
|
||||
r("232K", input_net, f"{ref}_ENUV", description="4.0-V nominal eFuse UV divider top")
|
||||
r("100K", f"{ref}_ENUV", "GND", description="eFuse UV divider bottom")
|
||||
r("383K", input_net, f"{ref}_OV", description="5.8-V nominal eFuse OV divider top")
|
||||
r("100K", f"{ref}_OV", "GND", description="eFuse OV divider bottom")
|
||||
r(ilim, f"{ref}_ILM", "GND", description="eFuse RILM=3334/ILIM")
|
||||
c("4.7nF", f"{ref}_DVDT", "GND", description="eFuse output slew control")
|
||||
c("1uF", input_net, "GND", description="eFuse local input bypass")
|
||||
c("10uF", output_net, "GND", description="eFuse local output bypass")
|
||||
tp(f"{ref}_FLT", f"{ref} FLT")
|
||||
|
||||
|
||||
add_efuse("U7", "VBUS_RAW", "USB5_PROT", "1.24K") # 2.69 A typical, <3 A max.
|
||||
add_efuse("U8", "EXT5_RAW", "EXT5_PROT", "825R") # 4.04 A typical.
|
||||
add_ic("U9", "TPS2121", "TPS2121RUXR", "Texas Instruments", "board:Texas_RUX0012A_VQFN12", [
|
||||
p(1, "OUT", "SYS5", "power_out"), p(2, "IN2", "EXT5_PROT", "power_in"), p(3, "CP2", "VCC3V3_AUX", "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(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")
|
||||
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("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("22.1K", "MUX_ILM", "GND", description="TPS2121 4.5-A typical output limit")
|
||||
c("10nF", "MUX_SS", "GND", description="TPS2121 soft-start")
|
||||
for _ in range(2):
|
||||
c("47uF", "SYS5", "GND", footprint="Capacitor_SMD:C_1210_3225Metric", description="5-V system bulk")
|
||||
|
||||
add_ic("U10", "SN74LVC2G02", "SN74LVC2G02DCUR", "Texas Instruments",
|
||||
"Package_SO:VSSOP-8_2.3x2mm_P0.5mm", [
|
||||
p(1, "1Y", "USB_3A_OK", "output"), p(2, "1A", "TUSB_OUT1", "input"), p(3, "1B", "TUSB_OUT2", "input"),
|
||||
p(4, "GND", "GND", "power_in"), p(5, "2A", "MUX_ST", "input"), p(6, "2B", "MUX_ST", "input"),
|
||||
p(7, "2Y", "EXT_SELECTED", "output"), p(8, "VCC", "VCC3V3_AUX", "power_in")],
|
||||
"USB 3-A advertisement NOR decode and mux-status inversion")
|
||||
add_ic("U11", "SN74LVC1G32", "SN74LVC1G32DBVR", "Texas Instruments", "Package_TO_SOT_SMD:SOT-23-5", [
|
||||
p(1, "A", "USB_3A_OK", "input"), p(2, "B", "EXT_SELECTED", "input"), p(3, "GND", "GND", "power_in"),
|
||||
p(4, "Y", "MAIN_EN", "output"), p(5, "VCC", "VCC3V3_AUX", "power_in")],
|
||||
"Main-rail enable OR: qualified 3-A USB or selected external 5 V")
|
||||
c("100nF", "VCC3V3_AUX", "GND", description="Power-qualification logic local bypass")
|
||||
c("100nF", "VCC3V3_AUX", "GND", description="Power qualification logic bypass")
|
||||
|
||||
|
||||
# Rail generation and Efinix sequencing.
|
||||
def add_ldo(ref: str, value: str, mpn: str, vin: str, vout: str, enable: str, desc: str) -> None:
|
||||
add_ic(ref, value, mpn, "Texas Instruments", "Package_TO_SOT_SMD:SOT-23-5", [
|
||||
p(1, "IN", vin, "power_in"), p(2, "GND", "GND", "power_in"), p(3, "EN", enable, "input"),
|
||||
p(4, "NC", None), p(5, "OUT", vout, "power_out")], desc)
|
||||
c("1uF", vin, "GND", description=f"{ref} local input capacitor")
|
||||
c("1uF", vout, "GND", description=f"{ref} low-noise LDO output capacitor")
|
||||
|
||||
|
||||
add_ldo("U12", "TPS7A2033", "TPS7A2033PDBVR", "SYS5", "VCC3V3_AUX", "SYS5",
|
||||
"Always-on 300-mA auxiliary 3.3-V LDO")
|
||||
add_ic("U13", "TPS6287B15HA0", "TPS6287B15HA0WRZVR", "Texas Instruments",
|
||||
"board:Texas_RZV0024A_WQFN24", [
|
||||
p(1, "VOSNS", "CORE_SENSE_P", "input"), p(2, "EN", "CORE_EN_R", "input"),
|
||||
p(3, "VSET1", "GND", "input"), p(4, "VSET2", "SYS5", "input"),
|
||||
p(5, "VIN", "SYS5", "power_in"), p(6, "VIN", "SYS5", "power_in"),
|
||||
p(7, "GND", "GND", "power_in"), p(8, "GND", "GND", "power_in"),
|
||||
p(9, "SW", "CORE_SW", "power_out"), p(10, "SW", "CORE_SW", "power_out"),
|
||||
p(11, "SW", "CORE_SW", "power_out"), p(12, "SW", "CORE_SW", "power_out"),
|
||||
p(13, "GND", "GND", "power_in"), p(14, "GND", "GND", "power_in"),
|
||||
p(15, "VIN", "SYS5", "power_in"), p(16, "VIN", "SYS5", "power_in"),
|
||||
p(17, "SYNCOUT", None, "output"), p(18, "MODE/SYNC", "SYS5", "input"),
|
||||
p(19, "SDA", "GND", "bidirectional"), p(20, "SCL", "GND", "bidirectional"),
|
||||
p(21, "PG", "CORE_PG", "open_collector"), p(22, "AGND", "GND", "power_in"),
|
||||
p(23, "COMP", "CORE_COMP", "input"), p(24, "GOSNS", "CORE_SENSE_N", "input"),
|
||||
p(25, "THERMAL_PADS", "GND", "power_in")],
|
||||
"15-A synchronous 0.95-V FPGA core/VCCA/VDD_SOC buck, fixed PWM")
|
||||
r("15K", "MAIN_EN", "CORE_EN_R", description="TPS6287B mandatory >=15-kohm EN series resistor")
|
||||
r("0R", "VCC0V95", "CORE_SENSE_P", description="Remote positive sense link at FPGA PDN")
|
||||
r("0R", "GND", "CORE_SENSE_N", description="Remote ground sense link at FPGA PDN")
|
||||
r("2.1K", "CORE_COMP", "CORE_COMP_RC", description="TPS6287B calculated compensation RZ")
|
||||
c("1.8nF", "CORE_COMP_RC", "GND", description="TPS6287B calculated compensation CC")
|
||||
c("10pF", "CORE_COMP", "GND", description="TPS6287B HF compensation bypass CC2")
|
||||
two_pin("L", "100nH", "XEL4030-101MEC", "Coilcraft", "board:Coilcraft_XEL4030",
|
||||
"CORE_SW", "VCC0V95", "Core inductor: 30-A saturation, 1.5-mohm DCR")
|
||||
for _ in range(4):
|
||||
c("22uF", "SYS5", "GND", mpn="GCM31CR71A226KE02L",
|
||||
footprint="Capacitor_SMD:C_1206_3216Metric", description="Core buck butterfly input capacitor")
|
||||
for _ in range(4):
|
||||
c("100uF", "VCC0V95", "GND", footprint="Capacitor_SMD:C_1210_3225Metric",
|
||||
description="Core output; 400-uF nominal, >=195-uF effective target")
|
||||
r("10K", "VCC3V3_AUX", "CORE_PG", description="Core buck PG pull-up")
|
||||
|
||||
|
||||
def add_tps62867(ref: str, output: str, sw: str, en: str, pg: str, vset: str, value: str) -> None:
|
||||
add_ic(ref, f"TPS62867 {value}", "TPS62867RQYR", "Texas Instruments",
|
||||
"board:Texas_RQY0009A_VQFN9", [
|
||||
p(1, "AGND", "GND", "power_in"), p(2, "PGND", "GND", "power_in"),
|
||||
p(3, "VIN", "SYS5", "power_in"), p(4, "EN", en, "input"),
|
||||
p(5, "PG", pg, "open_collector"), p(6, "VSET/MODE", f"{ref}_VSET", "input"),
|
||||
p(7, "SW", sw, "power_out"), p(8, "VOS", output, "input"), p(9, "FB", output, "input")],
|
||||
f"6-A synchronous {value} FPGA auxiliary/I/O buck")
|
||||
r(vset, f"{ref}_VSET", "GND", description=f"TPS62867 fixed {value} VSET resistor")
|
||||
two_pin("L", "240nH", "DFE201612E-R24M=P2", "Murata", "board:Murata_DFE201612E",
|
||||
sw, output, "TPS62867 manufacturer-recommended 6.6-A inductor")
|
||||
for _ in range(2):
|
||||
c("10uF", "SYS5", "GND", description=f"{ref} input capacitor")
|
||||
for _ in range(2):
|
||||
c("22uF", output, "GND", footprint="Capacitor_SMD:C_0805_2012Metric",
|
||||
description=f"{ref} output capacitor")
|
||||
|
||||
|
||||
add_tps62867("U14", "VCC1V8", "BUCK1V8_SW", "SEQ_FLAG1", "PG_1V8", "133K", "1.8 V")
|
||||
add_tps62867("U15", "VCC3V3", "BUCK3V3_SW", "SEQ_FLAG2", "PG_3V3", "205K", "3.3 V")
|
||||
add_ic("U16", "LM3880-1AA", "LM3880MF-1AA/NOPB", "Texas Instruments",
|
||||
"Package_TO_SOT_SMD:SOT-23-6", [
|
||||
p(1, "VCC", "VCC3V3_AUX", "power_in"), p(2, "GND", "GND", "power_in"),
|
||||
p(3, "EN", "CORE_PG", "input"), p(4, "FLAG3", "SEQ_FLAG3", "open_collector"),
|
||||
p(5, "FLAG2", "SEQ_FLAG2", "open_collector"), p(6, "FLAG1", "SEQ_FLAG1", "open_collector")],
|
||||
"Three-rail sequencer, 10-ms delay per rail")
|
||||
for net in ("SEQ_FLAG1", "SEQ_FLAG2"):
|
||||
r("10K", "VCC3V3_AUX", net, description="LM3880 open-drain flag pull-up")
|
||||
c("100nF", "VCC3V3_AUX", "GND", description="LM3880 bypass")
|
||||
|
||||
# Keep the four independent rail-valid indications separate from CRESET_N.
|
||||
# They are combined below by always-on logic; this prevents an intentional
|
||||
# configuration reset from corrupting the power-good state used to isolate the
|
||||
# FTDI drivers.
|
||||
r("10K", "VCC1V8", "FPGA_CRESET_N", description="FPGA CRESET_N configuration-rail pull-up")
|
||||
r("0R", "SEQ_FLAG3", "PWR_SEQ_OK", description="Sequencer final-flag sense link")
|
||||
r("0R", "PG_1V8", "PWR_1V8_OK", description="1.8-V buck power-good sense link")
|
||||
r("0R", "PG_3V3", "PWR_3V3_OK", description="3.3-V buck power-good sense link")
|
||||
|
||||
add_ldo("U17", "TPS7A2030", "TPS7A2030PDBVR", "VCC3V3", "VCC3V0_RF", "VCC3V3",
|
||||
"Dedicated low-noise 3.0-V MAX2771 analog LDO")
|
||||
add_ldo("U18", "TPS7A2030", "TPS7A2030PDBVR", "VCC3V3", "VCC3V0_DIG", "VCC3V3",
|
||||
"Dedicated low-noise 3.0-V MAX2771 digital and FPGA-bank LDO")
|
||||
add_ldo("U19", "TPS7A2030", "TPS7A2030PDBVR", "VCC3V3", "VCC3V0_CLK", "VCC3V3",
|
||||
"Dedicated low-noise 3.0-V oscillator LDO")
|
||||
add_ldo("U20", "TPS7A2033", "TPS7A2033PDBVR", "SYS5", "VCC3V3_ANT", "MAIN_EN",
|
||||
"Clean 3.3-V active-antenna bias LDO")
|
||||
|
||||
# The supervisor is powered from the always-on auxiliary rail, so its open-drain
|
||||
# output remains authoritative even while the monitored 3.0-V FPGA I/O rail is
|
||||
# absent. Its 20-ms (CT open) timeout also guarantees the configuration delay
|
||||
# after the last FPGA supply becomes valid. Power-good is kept separate from
|
||||
# user/FTDI reset so the JTAG buffers stay enabled during deliberate resets.
|
||||
add_ic("U28", "TPS3808G30", "TPS3808G30DBVR", "Texas Instruments",
|
||||
"Package_TO_SOT_SMD:SOT-23-6", [
|
||||
p(1, "RESET_N", "FPGA_POWER_OK_N", "open_collector"), p(2, "GND", "GND", "power_in"),
|
||||
p(3, "MR_N", "VCC3V3_AUX", "input"), p(4, "CT", None, "input"),
|
||||
p(5, "SENSE", "VCC3V0_DIG", "input"), p(6, "VDD", "VCC3V3_AUX", "power_in")],
|
||||
"3.0-V FPGA-bank supervisor; open-drain reset with 20-ms release delay")
|
||||
|
||||
# Always-on AND tree creates a true all-rail-valid signal. The fourth gate
|
||||
# includes LM3880 FLAG3, so both the programmed power-up interval and every
|
||||
# converter/supervisor indication must be valid before FPGA-facing FTDI
|
||||
# drivers are enabled or CRESET_N is released.
|
||||
add_ic("U34", "SN74LVC08A", "SN74LVC08APWR", "Texas Instruments",
|
||||
"Package_SO:TSSOP-14_4.4x5mm_P0.65mm", [
|
||||
p(1, "1A", "CORE_PG", "input"), p(2, "1B", "PWR_1V8_OK", "input"),
|
||||
p(3, "1Y", "PWR_OK_A", "output"),
|
||||
p(4, "2A", "PWR_3V3_OK", "input"), p(5, "2B", "FPGA_POWER_OK_N", "input"),
|
||||
p(6, "2Y", "PWR_OK_B", "output"), p(7, "GND", "GND", "power_in"),
|
||||
p(8, "3Y", "PWR_OK_C", "output"), p(9, "3A", "PWR_OK_A", "input"),
|
||||
p(10, "3B", "PWR_OK_B", "input"),
|
||||
p(11, "4Y", "ALL_FPGA_POWER_OK", "output"),
|
||||
p(12, "4A", "PWR_OK_C", "input"), p(13, "4B", "PWR_SEQ_OK", "input"),
|
||||
p(14, "VCC", "VCC3V3_AUX", "power_in")],
|
||||
"Always-on all-rail power-valid logic for reset release and debug isolation")
|
||||
|
||||
# Dense reference-design-style bypassing plus one bulk group per rail.
|
||||
for _ in range(18):
|
||||
c("100nF", "VCC0V95", "GND", description="Ti375 core/VCCA/VDD_SOC HF bypass")
|
||||
for _ in range(4):
|
||||
c("10uF", "VCC0V95", "GND", description="Ti375 core local bulk")
|
||||
for _ in range(6):
|
||||
c("100nF", "VCC1V8", "GND", description="Ti375 VCCAUX/HSIO/config bypass")
|
||||
for _ in range(2):
|
||||
c("10uF", "VCC1V8", "GND", description="Ti375 1.8-V local bulk")
|
||||
for rail in ("VCC3V3", "VCC3V0_DIG"):
|
||||
for _ in range(4):
|
||||
c("100nF", rail, "GND", description=f"Ti375 HVIO bank bypass on {rail}")
|
||||
c("10uF", rail, "GND", description=f"Ti375 I/O-rail bulk on {rail}")
|
||||
|
||||
|
||||
# MAX2771 direct MIXIN_HI path and OFF/3.3-V/5-V active-antenna bias.
|
||||
add_ic("J2", "GPS L1 SMA", "132289", "Amphenol RF",
|
||||
"Connector_Coaxial:SMA_Amphenol_132289_EdgeMount",
|
||||
[p(1, "RF", "RF_SMA"), p(2, "GND", "GND")],
|
||||
"Edge-launch 50-ohm active GNSS antenna input")
|
||||
components[-1].assembly = "THT"
|
||||
add_ic("D1", "ESD501", "ESD501DPYR", "Texas Instruments", "board:Texas_DPY0002A_X1SON",
|
||||
[p(1, "RF", "RF_SMA"), p(2, "GND", "GND")],
|
||||
"0.3-pF bidirectional 12-V RF ESD protector")
|
||||
c("27pF", "RF_SMA", "RF_PRE_SAW", mpn="600L270JT200T", footprint="board:Kyocera_600L_0402",
|
||||
description="High-Q 200-V C0G GNSS RF DC block")
|
||||
add_ic("FL1", "GPS L1/L1C SAW", "SF14-1582M5UUS2", "Kyocera", "board:Kyocera_SF14_5pad", [
|
||||
p(1, "IN", "RF_PRE_SAW"), p(2, "GND", "GND"), p(3, "GND", "GND"),
|
||||
p(4, "OUT", "RF_POST_SAW"), p(5, "GND", "GND")],
|
||||
"1565.19-1585.65-MHz SAW, 1.2-dB typical insertion loss")
|
||||
c("DNP", "RF_POST_SAW", "GND", mpn="", description="RF tuning shunt C1", dnp=True)
|
||||
r("0R", "RF_POST_SAW", "MAX_MIXIN_HI", description="RF tuning series link")
|
||||
c("DNP", "MAX_MIXIN_HI", "GND", mpn="", description="RF tuning shunt C2", dnp=True)
|
||||
|
||||
add_ic("J5", "ANT BIAS OFF/3V3/5V", "61300311121", "Wuerth Elektronik",
|
||||
"Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical", [
|
||||
p(1, "3V3", "VCC3V3_ANT", "power_in"), p(2, "SELECT", "ANT_BIAS_SEL", "power_in"),
|
||||
p(3, "5V", "SYS5", "power_in")],
|
||||
"One-shunt hardware antenna bias selection; no shunt is OFF")
|
||||
components[-1].assembly = "THT"
|
||||
add_ic("U27", "TPS22944", "TPS22944DCKR", "Texas Instruments",
|
||||
"Package_TO_SOT_SMD:SOT-353_SC-70-5", [
|
||||
p(1, "VOUT", "ANT_BIAS_SW", "power_out"), p(2, "GND", "GND", "power_in"),
|
||||
p(3, "OC", "ANT_BIAS_OC", "open_collector"), p(4, "ON", "ANT_BIAS_ON", "input"),
|
||||
p(5, "VIN", "ANT_BIAS_SEL", "power_in")],
|
||||
"Fixed 100-mA current-limited active-antenna load switch")
|
||||
r("100K", "ANT_BIAS_SEL", "ANT_BIAS_ON", description="Bias switch follows selected source")
|
||||
r("100K", "ANT_BIAS_ON", "GND", description="No-jumper default OFF")
|
||||
r("10K", "VCC3V3_AUX", "ANT_BIAS_OC", description="Antenna overcurrent pull-up")
|
||||
c("1uF", "ANT_BIAS_SEL", "GND", description="Bias-switch input bypass")
|
||||
c("1uF", "ANT_BIAS_SW", "GND", description="Bias-switch output bypass before choke")
|
||||
two_pin("L", "56nH", "0402HPH-56NXJRW", "Coilcraft", "Inductor_SMD:L_0402_1005Metric",
|
||||
"ANT_BIAS_SW", "RF_SMA", "L1 bias-tee choke: SRF 2.1 GHz, Q 64, 330 mA")
|
||||
tp("ANT_BIAS_OC", "ANT OC")
|
||||
|
||||
add_ic("U21", "MAX2771", "MAX2771ETI+", "Analog Devices",
|
||||
"Package_DFN_QFN:QFN-28-1EP_5x5mm_P0.5mm_EP3.25x3.25mm", [
|
||||
p(1, "CPOUT", "MAX_CPOUT", "output"), p(2, "VCCCP", "VCC3V0_RF", "power_in"),
|
||||
p(3, "XTAL", "MAX_XTAL", "input"), p(4, "CLKOUT", "MAX_CLKOUT_SRC", "output"),
|
||||
p(5, "ADC_CLKIN", "MAX_ADC_CLKIN", "input"), p(6, "VCCD", "VCC3V0_DIG", "power_in"),
|
||||
p(7, "SDATA", "MAX_SDATA_DEV", "bidirectional"), p(8, "SCLK", "MAX_SCLK_DEV", "input"),
|
||||
p(9, "CSN", "MAX_CSN_DEV", "input"), p(10, "Q1", "MAX_Q1_DEV", "output"),
|
||||
p(11, "Q0", "MAX_Q0_DEV", "output"), p(12, "I0", "MAX_I0_DEV", "output"),
|
||||
p(13, "I1", "MAX_I1_DEV", "output"), p(14, "VCCADC", "VCC3V0_DIG", "power_in"),
|
||||
p(15, "ANAIPOUT", None, "output"), p(16, "ANAINOUT", None, "output"),
|
||||
p(17, "LD", "MAX_LD_DEV", "output"), p(18, "SHDN", "MAX_SHDN_DEV", "input"),
|
||||
p(19, "LNAIN_HI", None, "input"), p(20, "LNAIN_LO", None, "input"), p(21, "NC", None),
|
||||
p(22, "VCCRF", "VCC3V0_RF", "power_in"), p(23, "LNAOUT_LO", None, "output"),
|
||||
p(24, "LNAOUT_HI", None, "output"), p(25, "MIXIN_HI", "MAX_MIXIN_HI", "input"),
|
||||
p(26, "MIXIN_LO", None, "input"), p(27, "VCCIF", "VCC3V0_RF", "power_in"),
|
||||
p(28, "VCCVCO", "VCC3V0_RF", "power_in"), p(29, "EP", "GND", "power_in")],
|
||||
"GNSS receiver; both LNAs disabled; direct high-band mixer input")
|
||||
|
||||
# Official typical topology: CPOUT has a small direct capacitor and an R-C
|
||||
# series integrating branch to ground.
|
||||
c("15pF", "MAX_CPOUT", "GND", description="MAX2771 PLL loop-filter shunt capacitor")
|
||||
r("15K", "MAX_CPOUT", "MAX_LOOP_RC", description="MAX2771 PLL loop-filter resistor")
|
||||
c("750pF", "MAX_LOOP_RC", "GND", description="MAX2771 PLL loop integrating capacitor")
|
||||
c("10nF", "TCXO16_OUT", "MAX_XTAL", description="MAX2771 external reference AC coupling")
|
||||
r("33R", "OSC40_OUT", "MAX_ADC_CLKIN", description="40-MHz ADC-clock source termination")
|
||||
for signal in ("MAX_CLKOUT", "MAX_I0", "MAX_I1", "MAX_Q0", "MAX_Q1", "MAX_LD"):
|
||||
source = f"{signal}_SRC" if signal == "MAX_CLKOUT" else f"{signal}_DEV"
|
||||
r("22R", source, signal, description="MAX2771 source-series digital damping")
|
||||
for signal in ("MAX_SCLK", "MAX_CSN", "MAX_SDATA", "MAX_SHDN"):
|
||||
r("22R", signal, f"{signal}_DEV", description="MAX2771 serial/control source damping")
|
||||
r("100K", "MAX_SHDN_DEV", "GND", description="MAX2771 remains shut down before FPGA configuration")
|
||||
for rail, count in (("VCC3V0_RF", 5), ("VCC3V0_DIG", 2)):
|
||||
for _ in range(count):
|
||||
c("100nF", rail, "GND", description=f"MAX2771 local {rail} bypass")
|
||||
c("1uF", rail, "GND", description=f"MAX2771 local {rail} bulk")
|
||||
|
||||
add_ic("U23", "16.368 MHz TCXO", "X1G005441030125", "Epson", "board:Epson_TG2016SMN", [
|
||||
p(1, "NC", None), p(2, "GND", "GND", "power_in"), p(3, "OUT", "TCXO16_OUT", "output"),
|
||||
p(4, "VCC", "VCC3V0_CLK", "power_in")],
|
||||
"TG2016SMN clipped-sine ±0.5-ppm MAX2771 reference")
|
||||
c("100nF", "VCC3V0_CLK", "GND", description="16.368-MHz TCXO bypass")
|
||||
c("1uF", "VCC3V0_CLK", "GND", description="Clock-rail local bulk")
|
||||
add_ic("U24", "40.000 MHz XO", "X1G004171041100", "Epson",
|
||||
"Oscillator:Oscillator_SMD_SeikoEpson_SG210-4Pin_2.5x2.0mm", [
|
||||
p(1, "ST", "VCC3V0_CLK", "input"), p(2, "GND", "GND", "power_in"),
|
||||
p(3, "OUT", "OSC40_OUT", "output"), p(4, "VCC", "VCC3V0_CLK", "power_in")],
|
||||
"SG-210STF ±25-ppm CMOS FPGA/system ADC oscillator")
|
||||
r("33R", "OSC40_OUT", "CLK_40M_FPGA", description="40-MHz FPGA-clock source termination")
|
||||
c("100nF", "VCC3V0_CLK", "GND", description="40-MHz oscillator bypass")
|
||||
|
||||
|
||||
# Exact Murata SCH16T-K01-1 SafeSPI pinout and carrier-board capacitors.
|
||||
add_ic("U22", "SCH16T-K01-1", "SCH16T-K01-1", "Murata", "board:Murata_SCH16T_K01", [
|
||||
p(1, "HS_GND", "GND", "power_in"), p(2, "RESERVED", None),
|
||||
p(3, "TA9_GND", "GND", "power_in"), p(4, "TA8_GND", "GND", "power_in"),
|
||||
p(5, "RESERVED_GND", "GND", "power_in"), p(6, "EXTRESN", "IMU_EXTRESN", "input"),
|
||||
p(7, "RESERVED_GND", "GND", "power_in"), p(8, "V3P3", "IMU_3V3", "power_in"),
|
||||
p(9, "GND", "GND", "power_in"), p(10, "VREGA2", "IMU_VREGA2", "power_out"),
|
||||
p(11, "VREGA", "IMU_VREGA2", "power_out"), p(12, "GND", "GND", "power_in"),
|
||||
p(13, "GND", "GND", "power_in"), p(14, "VREGD2", "IMU_VREGD", "power_out"),
|
||||
p(15, "VREGD", "IMU_VREGD", "power_out"), p(16, "GND", "GND", "power_in"),
|
||||
p(17, "V3P3", "IMU_3V3", "power_in"), p(18, "VDDIO", "VCC3V3", "power_in"),
|
||||
p(19, "MISO", "IMU_MISO_DEV", "output"), p(20, "DRY_SYNC", "IMU_DRY_SYNC", "bidirectional"),
|
||||
p(21, "SCK", "IMU_SCK_DEV", "input"), p(22, "CS", "IMU_CS_DEV", "input"),
|
||||
p(23, "MOSI", "IMU_MOSI_DEV", "input"), p(24, "HS_GND", "GND", "power_in")],
|
||||
"Six-DoF SafeSPI IMU, exact K01-1 variant")
|
||||
two_pin("FB", "600R@100MHz", "BLM15AG601SN1D", "Murata", "Inductor_SMD:L_0402_1005Metric",
|
||||
"VCC3V3", "IMU_3V3", "IMU supply ferrite isolation")
|
||||
for signal in ("IMU_SCK", "IMU_MOSI", "IMU_CS"):
|
||||
r("22R", signal, f"{signal}_DEV", description="SafeSPI source-series damping")
|
||||
r("22R", "IMU_MISO_DEV", "IMU_MISO", description="SafeSPI MISO source damping at IMU")
|
||||
r("10K", "IMU_EXTRESN", "GND", description="Hold IMU reset until FPGA releases it")
|
||||
c("100nF", "IMU_3V3", "GND", description="Murata V3P3 HF bypass")
|
||||
c("1uF", "IMU_3V3", "GND", description="Murata V3P3 local capacitor")
|
||||
c("10uF", "IMU_VREGA2", "GND", description="Murata required VREGA2 capacitor")
|
||||
c("1uF", "IMU_VREGD", "GND", description="Murata required VREGD capacitor")
|
||||
c("100nF", "VCC3V3", "GND", description="Murata VDDIO bypass")
|
||||
|
||||
|
||||
# FT4232H follows the Efinix development-kit interface order:
|
||||
# A/interface 0 = RISC-V JTAG, B/interface 1 = FPGA hard JTAG.
|
||||
ftdi_pins = [
|
||||
p(1, "EECS", "FTDI_EECS", "output"), p(2, "VCORE", "FTDI_1V8", "power_in"),
|
||||
p(3, "OSCI", "FTDI_OSCI", "input"), p(4, "OSCO", None, "output"),
|
||||
p(5, "VPHY", "FTDI_VPHY", "power_in"), p(6, "REF", "FTDI_REF", "input"),
|
||||
p(7, "DM", "FTDI_D_N", "bidirectional"), p(8, "DP", "FTDI_D_P", "bidirectional"),
|
||||
p(9, "VPLL", "FTDI_VPLL", "power_in"), p(10, "TEST", "GND", "input"),
|
||||
p(11, "RESET_N", "FTDI_RESET_N", "input"),
|
||||
p(12, "ADBUS0/TCK", "FTDI_A_TCK_SRC", "bidirectional"),
|
||||
p(13, "ADBUS1/TDI", "FTDI_A_TDI_SRC", "bidirectional"),
|
||||
p(14, "ADBUS2/TDO", "FTDI_A_TDO_SRC", "bidirectional"),
|
||||
p(15, "ADBUS3/TMS", "FTDI_A_TMS_SRC", "bidirectional"),
|
||||
p(16, "VCCIO", "VCC3V3_AUX", "power_in"),
|
||||
p(17, "ADBUS4", None, "bidirectional"), p(18, "ADBUS5", None, "bidirectional"),
|
||||
p(19, "ADBUS6", None, "bidirectional"), p(20, "ADBUS7", None, "bidirectional"),
|
||||
p(21, "GND", "GND", "power_in"),
|
||||
p(22, "BDBUS0/TCK", "FTDI_B_TCK_SRC", "bidirectional"),
|
||||
p(23, "BDBUS1/TDI", "FTDI_B_TDI_SRC", "bidirectional"),
|
||||
p(24, "BDBUS2/TDO", "FTDI_B_TDO_SRC", "bidirectional"),
|
||||
p(25, "BDBUS3/TMS", "FTDI_B_TMS_SRC", "bidirectional"),
|
||||
p(26, "BDBUS4", "FTDI_B_RESET_N_SRC", "bidirectional"), p(27, "BDBUS5", None, "bidirectional"),
|
||||
p(28, "BDBUS6", None, "bidirectional"), p(29, "BDBUS7", None, "bidirectional"),
|
||||
p(30, "SUSPEND_N", None, "output"), p(31, "VCORE", "FTDI_1V8", "power_in"),
|
||||
p(32, "CDBUS0/TXD", "FTDI_C_TXD_SRC", "bidirectional"),
|
||||
p(33, "CDBUS1/RXD", "FTDI_C_RXD_SRC", "bidirectional"),
|
||||
p(34, "CDBUS2", None, "bidirectional"), p(35, "CDBUS3", None, "bidirectional"),
|
||||
p(36, "VCCIO", "VCC3V3_AUX", "power_in"),
|
||||
p(37, "CDBUS4", None, "bidirectional"), p(38, "CDBUS5", None, "bidirectional"),
|
||||
p(39, "CDBUS6", None, "bidirectional"), p(40, "CDBUS7", None, "bidirectional"),
|
||||
p(41, "GND", "GND", "power_in"),
|
||||
p(42, "DDBUS0/TXD", "FTDI_D_TXD_SRC", "bidirectional"),
|
||||
p(43, "VREGOUT", "FTDI_1V8", "power_out"), p(44, "VREGIN", "VCC3V3_AUX", "power_in"),
|
||||
p(45, "GND", "GND", "power_in"),
|
||||
p(46, "DDBUS1/RXD", "FTDI_D_RXD_SRC", "bidirectional"),
|
||||
p(47, "DDBUS2/RTS", "FTDI_D_DBG_SRC", "bidirectional"),
|
||||
p(48, "DDBUS3", None, "bidirectional"), p(49, "DDBUS4", None, "bidirectional"),
|
||||
p(50, "VCCIO", "VCC3V3_AUX", "power_in"),
|
||||
p(51, "DDBUS5", None, "bidirectional"), p(52, "DDBUS6", None, "bidirectional"),
|
||||
p(53, "DDBUS7", None, "bidirectional"), p(54, "PWREN_N", None, "output"),
|
||||
p(55, "EEDATA", "FTDI_EEDATA", "bidirectional"), p(56, "EECLK", "FTDI_EECLK", "output"),
|
||||
p(57, "EP", "GND", "power_in")]
|
||||
add_ic("U2", "FT4232H-56Q", "FT4232H-56Q-REEL", "FTDI", "board:FTDI_VQFN56_EP5p9",
|
||||
ftdi_pins, "Quad high-speed USB bridge; all four channels assigned")
|
||||
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("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("10K", "FTDI_RESET_N", "GND", description="Self-powered VBUS-present reset divider bottom")
|
||||
for _ in range(2):
|
||||
c("100nF", "VCC3V3_AUX", "GND", description="FT4232H VCCIO/VREGIN bypass")
|
||||
for _ in range(2):
|
||||
c("100nF", "FTDI_1V8", "GND", description="FT4232H VCORE bypass")
|
||||
c("1uF", "FTDI_1V8", "GND", description="FT4232H regulator output capacitor")
|
||||
two_pin("FB", "600R@100MHz", "BLM15AG601SN1D", "Murata", "Inductor_SMD:L_0402_1005Metric",
|
||||
"VCC3V3_AUX", "FTDI_VPHY", "FTDI USB-PHY supply ferrite")
|
||||
two_pin("FB", "600R@100MHz", "BLM15AG601SN1D", "Murata", "Inductor_SMD:L_0402_1005Metric",
|
||||
"VCC3V3_AUX", "FTDI_VPLL", "FTDI PLL supply ferrite")
|
||||
c("100nF", "FTDI_VPHY", "GND", description="FTDI VPHY bypass")
|
||||
c("100nF", "FTDI_VPLL", "GND", description="FTDI VPLL bypass")
|
||||
|
||||
add_ic("U25", "12.000 MHz XO", "ECS-2520MV-120-BN-TR", "ECS",
|
||||
"Oscillator:Oscillator_SMD_ECS_2520MV-xxx-xx-4Pin_2.5x2.0mm", [
|
||||
p(1, "OE", "VCC3V3_AUX", "input"), p(2, "GND", "GND", "power_in"),
|
||||
p(3, "OUT", "FTDI_OSC12", "output"), p(4, "VDD", "VCC3V3_AUX", "power_in")],
|
||||
"Current-production 12-MHz CMOS FT4232H clock")
|
||||
r("33R", "FTDI_OSC12", "FTDI_OSCI", description="FTDI oscillator source termination")
|
||||
c("100nF", "VCC3V3_AUX", "GND", description="FTDI oscillator local bypass")
|
||||
|
||||
add_ic("U3", "93LC56B x16", "93LC56BT-I/OT", "Microchip", "Package_TO_SOT_SMD:SOT-23-6", [
|
||||
p(1, "CS", "FTDI_EECS", "input"), p(2, "CLK", "FTDI_EECLK", "input"),
|
||||
p(3, "DI", "FTDI_EEDATA", "input"), p(4, "DO", "FTDI_EEDO", "output"),
|
||||
p(5, "VSS", "GND", "power_in"), p(6, "VCC", "VCC3V3_AUX", "power_in")],
|
||||
"FTDI descriptor/channel-mode EEPROM, fixed 16-bit organization")
|
||||
r("2.2K", "FTDI_EEDO", "FTDI_EEDATA", description="EEPROM shared-data isolation")
|
||||
r("10K", "FTDI_EEDO", "VCC3V3_AUX", description="EEPROM DO pull-up")
|
||||
c("100nF", "VCC3V3_AUX", "GND", description="FTDI EEPROM bypass")
|
||||
|
||||
# FTDI is intentionally powered from the always-on debug rail. All signals
|
||||
# that can drive the FPGA therefore pass through Ioff-capable buffers powered
|
||||
# by the FPGA's 3.3-V I/O rail. When the FPGA rail is absent, the buffer
|
||||
# outputs are high impedance and no unpowered FPGA pin can be back-driven.
|
||||
add_ic("U29", "SN74LVC244A", "SN74LVC244APWR", "Texas Instruments",
|
||||
"Package_SO:TSSOP-20_4.4x6.5mm_P0.65mm", [
|
||||
p(1, "1OE_N", "FTDI_FPGA_BUF_OE_N", "input"),
|
||||
p(2, "1A1", "FTDI_A_TCK_SRC", "input"), p(18, "1Y1", "BUF_A_TCK", "output"),
|
||||
p(4, "1A2", "FTDI_A_TDI_SRC", "input"), p(16, "1Y2", "BUF_A_TDI", "output"),
|
||||
p(6, "1A3", "FTDI_A_TMS_SRC", "input"), p(14, "1Y3", "BUF_A_TMS", "output"),
|
||||
p(8, "1A4", "FTDI_B_TCK_SRC", "input"), p(12, "1Y4", "BUF_B_TCK", "output"),
|
||||
p(19, "2OE_N", "FTDI_FPGA_BUF_OE_N", "input"),
|
||||
p(11, "2A1", "FTDI_B_TDI_SRC", "input"), p(9, "2Y1", "BUF_B_TDI", "output"),
|
||||
p(13, "2A2", "FTDI_B_TMS_SRC", "input"), p(7, "2Y2", "BUF_B_TMS", "output"),
|
||||
p(15, "2A3", "FTDI_C_TXD_SRC", "input"), p(5, "2Y3", "BUF_C_TXD", "output"),
|
||||
p(17, "2A4", "FTDI_D_TXD_SRC", "input"), p(3, "2Y4", "BUF_D_TXD", "output"),
|
||||
p(10, "GND", "GND", "power_in"), p(20, "VCC", "VCC3V3", "power_in")],
|
||||
"Ioff-protected FPGA-input buffer; automatically isolates when FPGA 3.3 V is off")
|
||||
add_ic("U30", "SN74LVC1G125", "SN74LVC1G125DBVR", "Texas Instruments",
|
||||
"Package_TO_SOT_SMD:SOT-23-5", [p(1, "OE_N", "FTDI_FPGA_BUF_OE_N", "input"),
|
||||
p(2, "A", "FTDI_D_DBG_SRC", "input"),
|
||||
p(3, "GND", "GND", "power_in"), p(4, "Y", "BUF_D_DBG", "output"),
|
||||
p(5, "VCC", "VCC3V3", "power_in")],
|
||||
"Ioff-protected tri-state debug-GPIO buffer powered from FPGA 3.3 V")
|
||||
add_ic("U31", "SN74LVC1G07", "SN74LVC1G07DBVR", "Texas Instruments",
|
||||
"Package_TO_SOT_SMD:SOT-23-5", [p(1, "NC", None), p(2, "A", "FTDI_B_RESET_N_SRC", "input"),
|
||||
p(3, "GND", "GND", "power_in"), p(4, "Y_OD", "FPGA_CRESET_N", "open_collector"),
|
||||
p(5, "VCC", "VCC3V3_AUX", "power_in")],
|
||||
"Open-drain FTDI reset request into FPGA CRESET_N")
|
||||
add_ic("U32", "SN74LVC1G07", "SN74LVC1G07DBVR", "Texas Instruments",
|
||||
"Package_TO_SOT_SMD:SOT-23-5", [p(1, "NC", None), p(2, "A", "ALL_FPGA_POWER_OK", "input"),
|
||||
p(3, "GND", "GND", "power_in"), p(4, "Y_OD", "FPGA_CRESET_N", "open_collector"),
|
||||
p(5, "VCC", "VCC3V3_AUX", "power_in")],
|
||||
"Open-drain all-rail-valid translation into FPGA CRESET_N")
|
||||
add_ic("U33", "SN74LVC1G07", "SN74LVC1G07DBVR", "Texas Instruments",
|
||||
"Package_TO_SOT_SMD:SOT-23-5", [p(1, "NC", None), p(2, "A", "EXT_RESET_FILTER_N", "input"),
|
||||
p(3, "GND", "GND", "power_in"), p(4, "Y_OD", "FPGA_CRESET_N", "open_collector"),
|
||||
p(5, "VCC", "VCC1V8", "power_in")],
|
||||
"Open-drain external/manual reset isolation into FPGA CRESET_N")
|
||||
add_ic("Q1", "MMBT3904", "MMBT3904LT1G", "onsemi", "Package_TO_SOT_SMD:SOT-23", [
|
||||
p(1, "B", "FTDI_BUF_OE_BASE", "input"), p(2, "E", "GND", "power_in"),
|
||||
p(3, "C", "FTDI_FPGA_BUF_OE_N", "open_collector")],
|
||||
"Power-good inverter: enables FTDI-to-FPGA buffers only after all rails are valid")
|
||||
|
||||
for src, dst in (("BUF_B_TCK", "HARD_TCK"), ("BUF_B_TMS", "HARD_TMS"),
|
||||
("BUF_B_TDI", "HARD_TDI"), ("HARD_TDO", "FTDI_B_TDO_SRC")):
|
||||
r("33R", src, dst, description="FTDI interface-1 hard-JTAG source damping")
|
||||
r("10K", "VCC3V3", "HARD_TMS", description="Efinix-required hard-JTAG weak pull-up")
|
||||
c("100nF", "VCC3V3_AUX", "GND", description="Additional FT4232H channel-A VCCIO bypass")
|
||||
for src, dst in (("BUF_A_TCK", "RISCV_TCK"), ("BUF_A_TDI", "RISCV_TDI"),
|
||||
("BUF_A_TMS", "RISCV_TMS"), ("RISCV_TDO", "FTDI_A_TDO_SRC"),
|
||||
("BUF_C_TXD", "UART0_RX"), ("UART0_TX", "FTDI_C_RXD_SRC"),
|
||||
("BUF_D_TXD", "UART1_RX"), ("UART1_TX", "FTDI_D_RXD_SRC"),
|
||||
("BUF_D_DBG", "FTDI_DBG_GPIO")):
|
||||
r("33R", src, dst, description="FT4232H channel signal damping")
|
||||
|
||||
# Active-serial flash and four compact Digilent-format PMOD connectors.
|
||||
add_ic("U26", "512-Mbit QSPI flash", "IS25WP512M-JLLE", "ISSI",
|
||||
"Package_SON:WSON-8-1EP_6x5mm_P1.27mm_EP3.4x4.3mm", [
|
||||
p(1, "CS_N", "QSPI_CS_N", "input"), p(2, "SO/IO1", "QSPI_IO1", "bidirectional"),
|
||||
p(3, "WP_N/IO2", "QSPI_IO2", "bidirectional"), p(4, "GND", "GND", "power_in"),
|
||||
p(5, "SI/IO0", "QSPI_IO0", "bidirectional"), p(6, "SCLK", "QSPI_SCLK", "input"),
|
||||
p(7, "HOLD_N/IO3", "QSPI_IO3", "bidirectional"), p(8, "VCC", "VCC1V8", "power_in"),
|
||||
p(9, "EP", "GND", "power_in")],
|
||||
"1.8-V 512-Mbit FPGA active-serial configuration flash")
|
||||
r("10K", "VCC1V8", "QSPI_CS_N", description="Configuration flash CS pull-up")
|
||||
r("10K", "VCC1V8", "FPGA_TEST_N", description="Efinix mandatory TEST_N weak pull-up")
|
||||
r("10K", "VCC1V8", "FPGA_CSI", description="Efinix mandatory CSI weak pull-up")
|
||||
for net in ("QSPI_IO2", "QSPI_IO3"):
|
||||
r("10K", "VCC1V8", net, description="Configuration flash hold/write-protect pull-up")
|
||||
c("100nF", "VCC1V8", "GND", description="Configuration flash bypass")
|
||||
c("1uF", "VCC1V8", "GND", description="Configuration flash local bulk")
|
||||
|
||||
|
||||
def add_pmod(ref: str, prefix: str, rail: str, voltage: str) -> None:
|
||||
pins = [p(i, f"IO{i}", f"{prefix}_{i}") for i in (1, 2, 3, 4)]
|
||||
pins += [p(5, "GND", "GND"), p(6, voltage, rail)]
|
||||
pins += [p(i, f"IO{i}", f"{prefix}_{i}") for i in (7, 8, 9, 10)]
|
||||
pins += [p(11, "GND", "GND"), p(12, voltage, rail)]
|
||||
add_ic(ref, f"PMOD {voltage}", "67997-412HLF", "Amphenol ICC",
|
||||
"board:Digilent_PMOD_2x6_P2.54mm", pins,
|
||||
f"Digilent 2x6 PMOD: eight {voltage} GPIO, two GND, two {voltage}")
|
||||
components[-1].assembly = "THT"
|
||||
|
||||
|
||||
add_pmod("JPMOD1", "PMOD1", "VCC3V3", "3.3 V")
|
||||
add_pmod("JPMOD2", "PMOD2", "VCC3V3", "3.3 V")
|
||||
add_pmod("JPMOD3", "PMOD3", "VCC1V8", "1.8 V")
|
||||
add_pmod("JPMOD4", "PMOD4", "VCC1V8", "1.8 V")
|
||||
|
||||
for net, label in (("VBUS_RAW", "USB VBUS"), ("EXT5_RAW", "EXT 5V"), ("SYS5", "SYS 5V"),
|
||||
("VCC3V3_AUX", "AUX 3V3"), ("VCC0V95", "CORE 0V95"),
|
||||
("VCC1V8", "1V8"), ("VCC3V3", "3V3"), ("VCC3V0_RF", "RF 3V0"),
|
||||
("VCC3V0_DIG", "DIG 3V0"), ("FPGA_CRESET_N", "CRESET_N"),
|
||||
("FPGA_CDONE", "CDONE"), ("GND", "GND")):
|
||||
tp(net, label)
|
||||
|
||||
# Configuration/JTAG pulls are located on the FPGA side of the isolation
|
||||
# buffers and use the voltage of their respective I/O bank.
|
||||
r("10K", "VCC1V8", "FPGA_CDONE", description="Efinix-required CDONE open-drain pull-up")
|
||||
for net in ("HARD_TCK", "HARD_TDI", "HARD_TDO"):
|
||||
r("10K", "VCC3V3", net, description="Efinix-required hard-JTAG weak pull-up")
|
||||
for net in ("RISCV_TCK", "RISCV_TMS", "RISCV_TDI", "RISCV_TDO"):
|
||||
r("10K", "VCC3V3", net, description="RISC-V JTAG idle-state pull-up")
|
||||
|
||||
# Supervisor support and reset-request fan-in. FTDI BDBUS4 uses the
|
||||
# Efinix-compatible active-low convention. Independent open-drain paths avoid
|
||||
# output contention and allow JTAG to remain live while CRESET_N is asserted.
|
||||
r("10K", "VCC3V3_AUX", "FTDI_B_RESET_N_SRC", description="FTDI FPGA-reset request default release")
|
||||
r("10K", "VCC3V3_AUX", "FPGA_POWER_OK_N", description="TPS3808 open-drain power-good pull-up")
|
||||
r("10K", "VCC3V3", "FTDI_FPGA_BUF_OE_N", description="Keep FTDI-to-FPGA buffers disabled until power-good")
|
||||
r("10K", "ALL_FPGA_POWER_OK", "FTDI_BUF_OE_BASE", description="All-rail-good inverter base-current limiter")
|
||||
r("100K", "FTDI_BUF_OE_BASE", "GND", description="Power-good inverter default-off base pull-down")
|
||||
r("10K", "VCC1V8", "EXT_RESET_FILTER_N", description="External/manual reset input pull-up")
|
||||
r("1K", "EXT_RESET_N", "EXT_RESET_FILTER_N", description="External-reset input current and ESD limiting")
|
||||
r("10K", "VCC3V3_AUX", "PWR_SEQ_OK", description="LM3880 final-flag pull-up for all-power logic")
|
||||
r("10K", "VCC3V3_AUX", "PWR_1V8_OK", description="1.8-V power-good pull-up for all-power logic")
|
||||
r("10K", "VCC3V3_AUX", "PWR_3V3_OK", description="3.3-V power-good pull-up for all-power logic")
|
||||
c("4.7nF", "VCC3V0_DIG", "GND", description="TPS3808 SENSE transient filter")
|
||||
c("4.7nF", "EXT_RESET_FILTER_N", "GND", description="Manual/external reset cable-noise filter")
|
||||
c("100nF", "VCC3V3_AUX", "GND", description="TPS3808 local bypass")
|
||||
c("100nF", "VCC3V3", "GND", description="SN74LVC244A local bypass")
|
||||
c("100nF", "VCC3V3", "GND", description="SN74LVC1G125 local bypass")
|
||||
c("100nF", "VCC3V3_AUX", "GND", description="SN74LVC1G07 local bypass")
|
||||
c("100nF", "VCC3V3_AUX", "GND", description="Power-good open-drain buffer local bypass")
|
||||
c("100nF", "VCC1V8", "GND", description="External-reset open-drain buffer local bypass")
|
||||
c("100nF", "VCC3V3_AUX", "GND", description="All-rail-valid logic local bypass")
|
||||
|
||||
add_ic("J4", "EXT RESET_N", "SM02B-SRSS-TB(LF)(SN)", "JST",
|
||||
"Connector_JST:JST_SH_SM02B-SRSS-TB_1x02-1MP_P1.00mm_Horizontal", [
|
||||
p(1, "EXT_RESET_N", "EXT_RESET_N", "input"), p(2, "GND", "GND", "power_in")],
|
||||
"External active-low open-drain reset input; JST-SH 1.0-mm")
|
||||
add_ic("SW1", "FPGA RESET", "B3U-1000P", "Omron",
|
||||
"Button_Switch_SMD:SW_SPST_B3U-1000P", [
|
||||
p(1, "RESET", "EXT_RESET_FILTER_N", "passive"), p(2, "GND", "GND", "passive")],
|
||||
"User-accessible momentary FPGA configuration reset")
|
||||
add_ic("D2", "RESET ESD", "ESD5Z3.3T1G", "onsemi", "Diode_SMD:D_SOD-523", [
|
||||
p(1, "K", "EXT_RESET_N", "passive"), p(2, "A", "GND", "passive")],
|
||||
"External-reset connector ESD clamp")
|
||||
|
||||
|
||||
def validate() -> None:
|
||||
refs = [x.ref for x in components]
|
||||
assert len(refs) == len(set(refs))
|
||||
u1 = next(x for x in components if x.ref == "U1")
|
||||
assert len(u1.pins) == 529 and len({x.number for x in u1.pins}) == 529
|
||||
assert next(x for x in u1.pins if x.number == "G10").net == "HARD_TCK"
|
||||
assert {pin.number: pin.net for pin in u1.pins if pin.number in {
|
||||
"G10", "G12", "G9", "G13", "F12", "V19", "P18", "T18"
|
||||
}} == {
|
||||
"G10": "HARD_TCK", "G12": "HARD_TMS", "G9": "HARD_TDI", "G13": "HARD_TDO",
|
||||
"F12": "RISCV_TCK", "V19": "RISCV_TDI", "P18": "RISCV_TMS", "T18": "RISCV_TDO",
|
||||
}
|
||||
assert next(x for x in u1.pins if x.number == "B11").net == "MAX_CLKOUT"
|
||||
maxic = next(x for x in components if x.ref == "U21")
|
||||
assert next(x for x in maxic.pins if x.number == "25").net == "MAX_MIXIN_HI"
|
||||
assert all(next(x for x in maxic.pins if x.number == n).net is None
|
||||
for n in ("19", "20", "23", "24", "26"))
|
||||
pmods = [next(x for x in components if x.ref == f"JPMOD{i}") for i in range(1, 5)]
|
||||
assert all(len(x.pins) == 12 for x in pmods)
|
||||
assert all(sum(pin.net == "GND" for pin in x.pins) == 2 for x in pmods)
|
||||
assert all(sum((pin.net or "").startswith("PMOD") for pin in x.pins) == 8 for x in pmods)
|
||||
assert all(sum(pin.net == "VCC3V3" for pin in x.pins) == 2 for x in pmods[:2])
|
||||
assert all(sum(pin.net == "VCC1V8" for pin in x.pins) == 2 for x in pmods[2:])
|
||||
assert not any((pin.net or "").startswith("EXP_") for pin in u1.pins)
|
||||
u2 = next(x for x in components if x.ref == "U2")
|
||||
assert {pin.number: pin.net for pin in u2.pins if pin.number in {
|
||||
"12", "13", "14", "15", "22", "23", "24", "25"
|
||||
}} == {
|
||||
"12": "FTDI_A_TCK_SRC", "13": "FTDI_A_TDI_SRC",
|
||||
"14": "FTDI_A_TDO_SRC", "15": "FTDI_A_TMS_SRC",
|
||||
"22": "FTDI_B_TCK_SRC", "23": "FTDI_B_TDI_SRC",
|
||||
"24": "FTDI_B_TDO_SRC", "25": "FTDI_B_TMS_SRC",
|
||||
}
|
||||
assert not any(c.ref in {"U4", "J6"} for c in components)
|
||||
assert not any((pin.net or "").startswith("FTDI_HARD_") or pin.net == "JTAG_SW_OE_N"
|
||||
for c in components for pin in c.pins)
|
||||
|
||||
def has_link(net_a: str, net_b: str, value: str = "33R") -> bool:
|
||||
return any(c.value == value and len(c.pins) == 2 and
|
||||
{c.pins[0].net, c.pins[1].net} == {net_a, net_b}
|
||||
for c in components)
|
||||
|
||||
for src, dst in (
|
||||
("BUF_A_TCK", "RISCV_TCK"), ("BUF_A_TMS", "RISCV_TMS"),
|
||||
("BUF_A_TDI", "RISCV_TDI"), ("FTDI_A_TDO_SRC", "RISCV_TDO"),
|
||||
("BUF_B_TCK", "HARD_TCK"), ("BUF_B_TMS", "HARD_TMS"),
|
||||
("BUF_B_TDI", "HARD_TDI"), ("FTDI_B_TDO_SRC", "HARD_TDO"),
|
||||
):
|
||||
assert has_link(src, dst), (src, dst)
|
||||
assert next(pin for pin in u2.pins if pin.number == "26").net == "FTDI_B_RESET_N_SRC"
|
||||
u28 = next(x for x in components if x.ref == "U28")
|
||||
assert {pin.name: pin.net for pin in u28.pins} == {
|
||||
"RESET_N": "FPGA_POWER_OK_N", "GND": "GND", "MR_N": "VCC3V3_AUX",
|
||||
"CT": None, "SENSE": "VCC3V0_DIG", "VDD": "VCC3V3_AUX"}
|
||||
u34 = next(x for x in components if x.ref == "U34")
|
||||
assert {pin.number: pin.net for pin in u34.pins if pin.number in {"1", "2", "4", "5", "12", "13"}} == {
|
||||
"1": "CORE_PG", "2": "PWR_1V8_OK", "4": "PWR_3V3_OK",
|
||||
"5": "FPGA_POWER_OK_N", "12": "PWR_OK_C", "13": "PWR_SEQ_OK"}
|
||||
assert next(pin for pin in next(x for x in components if x.ref == "U32").pins
|
||||
if pin.number == "2").net == "ALL_FPGA_POWER_OK"
|
||||
assert not any(c.value == "0R" and "FPGA_CRESET_N" in {pin.net for pin in c.pins}
|
||||
for c in components)
|
||||
for net, rail in (("FPGA_CDONE", "VCC1V8"), ("HARD_TCK", "VCC3V3"),
|
||||
("HARD_TMS", "VCC3V3"), ("HARD_TDI", "VCC3V3"),
|
||||
("HARD_TDO", "VCC3V3")):
|
||||
assert has_link(net, rail, "10K"), (net, rail)
|
||||
assert {pin.net for pin in next(x for x in components if x.ref == "J4").pins} == {
|
||||
"EXT_RESET_N", "GND"}
|
||||
assert [x.ref for x in components if x.ref.startswith("TP")] == [f"TP{i}" for i in range(1, 16)]
|
||||
j5 = next(x for x in components if x.ref == "J5")
|
||||
assert {x.net for x in j5.pins} == {"VCC3V3_ANT", "ANT_BIAS_SEL", "SYS5"}
|
||||
|
||||
|
||||
validate()
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export a routing-stage DSN containing only selected net classes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
import pcbnew
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("board", type=Path)
|
||||
parser.add_argument("dsn", type=Path)
|
||||
parser.add_argument("--classes", required=True,
|
||||
help="Exact KiCad net-class names separated by ';' (or ',' for legacy single names)")
|
||||
parser.add_argument("--preview", type=Path,
|
||||
help="Optional filtered KiCad board for inspection")
|
||||
args = parser.parse_args()
|
||||
|
||||
# KiCad can report a compound effective class such as "CLOCK,POWER" when
|
||||
# two pattern assignments match one net. Semicolons let that exact class
|
||||
# name pass through without being split.
|
||||
separator = ";" if ";" in args.classes else ","
|
||||
keep = {name.strip() for name in args.classes.split(separator) if name.strip()}
|
||||
board = pcbnew.LoadBoard(str(args.board))
|
||||
kept_pads = 0
|
||||
omitted_pads = 0
|
||||
kept_nets: set[str] = set()
|
||||
for fp in board.GetFootprints():
|
||||
for pad in fp.Pads():
|
||||
if not pad.GetNetname():
|
||||
continue
|
||||
if pad.GetNetClassName() in keep:
|
||||
kept_pads += 1
|
||||
kept_nets.add(pad.GetNetname())
|
||||
else:
|
||||
pad.SetNetCode(0)
|
||||
omitted_pads += 1
|
||||
board.BuildListOfNets()
|
||||
if args.preview:
|
||||
pcbnew.SaveBoard(str(args.preview), board)
|
||||
if not pcbnew.ExportSpecctraDSN(board, str(args.dsn)):
|
||||
raise RuntimeError(f"Failed to export {args.dsn}")
|
||||
print(f"Classes: {', '.join(sorted(keep))}")
|
||||
print(f"Kept {kept_pads} pads on {len(kept_nets)} nets; omitted {omitted_pads} pads")
|
||||
print(f"Saved {args.dsn}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export the authoritative Ti375 C529 package map from Efinity.
|
||||
|
||||
Run with Efinity's bundled Python because the resource pickle contains Efinity
|
||||
classes. No pin name or ball assignment is embedded in this script.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import gzip
|
||||
import hashlib
|
||||
import json
|
||||
import pickle
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
HSIO_BANKS = {"2A", "2B", "2C", "2D", "2E", "4A_4B", "4C", "4D"}
|
||||
HVIO_BANKS = {"BL2_BL3", "BR0", "BR3_BR4", "TL1_TL5", "TR0", "TR1", "TR2"}
|
||||
|
||||
|
||||
def natural_ball(ball: str) -> tuple[str, int]:
|
||||
match = re.fullmatch(r"([A-Z]+)([0-9]+)", ball)
|
||||
if not match:
|
||||
return ball, 0
|
||||
return match.group(1), int(match.group(2))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--db",
|
||||
type=Path,
|
||||
default=Path("/tools/efinix/efinity/2025.2/pt/db/resource/Ti375C529.db"),
|
||||
)
|
||||
parser.add_argument("--csv", type=Path, default=Path("docs/ti375_c529_package.csv"))
|
||||
parser.add_argument("--meta", type=Path, default=Path("docs/ti375_c529_package.meta.json"))
|
||||
args = parser.parse_args()
|
||||
|
||||
with gzip.open(args.db, "rb") as stream:
|
||||
device = pickle.load(stream)
|
||||
|
||||
package = device.get_package()
|
||||
io_db = device._PeripheryDevice__io_pad
|
||||
io_by_pad = {
|
||||
vars(item)["_IOPadInfo__pad_name"]: vars(item)
|
||||
for item in io_db.get_all_io_pads()
|
||||
}
|
||||
|
||||
rows: list[dict[str, str]] = []
|
||||
for ball, mapping in sorted(package.get_package_pad_map().items(), key=lambda pair: natural_ball(pair[0])):
|
||||
pad = mapping.get_pad_name()
|
||||
info = io_by_pad.get(pad, {})
|
||||
die_bank = info.get("_IOPadInfo__bank_name", "")
|
||||
package_bank = info.get("_IOPadInfo__pkg_bank_name", "")
|
||||
direction_obj = info.get("_IOPadInfo__direction")
|
||||
direction = getattr(direction_obj, "name", "")
|
||||
instances = info.get("_IOPadInfo__instances", [])
|
||||
|
||||
if package_bank in HSIO_BANKS:
|
||||
bank_class = "HSIO"
|
||||
allowed_vccio = "1.2;1.35;1.5;1.8"
|
||||
elif package_bank in HVIO_BANKS:
|
||||
bank_class = "HVIO"
|
||||
allowed_vccio = "1.8;2.5;3.0;3.3"
|
||||
else:
|
||||
bank_class = "HARD/POWER"
|
||||
allowed_vccio = ""
|
||||
|
||||
special: list[str] = []
|
||||
upper_pad = pad.upper()
|
||||
if "CLK" in upper_pad:
|
||||
special.append("CLOCK_CAPABLE")
|
||||
if "PLLIN" in upper_pad:
|
||||
special.append("PLL_INPUT")
|
||||
if "EXTFB" in upper_pad:
|
||||
special.append("PLL_EXTFB")
|
||||
if "CDI" in upper_pad:
|
||||
special.append("CORE_DATA_INPUT")
|
||||
if "EXTSPI" in upper_pad:
|
||||
special.append("EXT_SPI")
|
||||
if "CBSEL" in upper_pad:
|
||||
special.append("CONFIG_BOOT_SELECT")
|
||||
if re.search(r"_(P|N)(?:_|$)", upper_pad):
|
||||
special.append("DIFFERENTIAL_MEMBER")
|
||||
|
||||
is_fabric_gpio = pad.startswith(("GPIOB_", "GPIOT_", "GPIOL_", "GPIOR_"))
|
||||
rows.append(
|
||||
{
|
||||
"ball": ball,
|
||||
"official_pad_name": pad,
|
||||
"die_bank": die_bank,
|
||||
"package_bank": package_bank,
|
||||
"bank_class": bank_class,
|
||||
"allowed_vccio_v": allowed_vccio,
|
||||
"direction": direction,
|
||||
"function": info.get("_IOPadInfo__function", ""),
|
||||
"periphery_instances": ";".join(instances),
|
||||
"instance_pin": info.get("_IOPadInfo__pin", "") or "",
|
||||
"special_capability": ";".join(special),
|
||||
"fabric_gpio": "yes" if is_fabric_gpio else "no",
|
||||
}
|
||||
)
|
||||
|
||||
args.csv.parent.mkdir(parents=True, exist_ok=True)
|
||||
with args.csv.open("w", newline="", encoding="utf-8") as stream:
|
||||
writer = csv.DictWriter(stream, fieldnames=list(rows[0]))
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
|
||||
digest = hashlib.sha256(args.db.read_bytes()).hexdigest()
|
||||
metadata = {
|
||||
"device": device.get_device_name(),
|
||||
"mapped_device": device.get_device_mapped_name(),
|
||||
"package": package.get_package_name(),
|
||||
"package_status": package.get_status_str(),
|
||||
"source_database": str(args.db),
|
||||
"source_sha256": digest,
|
||||
"row_count": len(rows),
|
||||
"fabric_gpio_count": sum(row["fabric_gpio"] == "yes" for row in rows),
|
||||
"package_banks": package.get_package_bank_names(),
|
||||
}
|
||||
args.meta.write_text(json.dumps(metadata, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Import a Specctra session and install audited RF/USB copper.
|
||||
|
||||
Freerouting is used only for the dense non-critical interconnect. The GPS
|
||||
front end and USB2 pair are removed from that result and rebuilt here with
|
||||
deterministic geometry derived from the selected JLC08161H-3313 stackup.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import pcbnew
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
PLACED = ROOT / "hardware" / "board.kicad_pcb"
|
||||
ROUTED = ROOT / "hardware" / "board_routed.kicad_pcb"
|
||||
|
||||
RF_NETS = {"RF_SMA", "RF_PRE_SAW", "RF_POST_SAW", "MAX_MIXIN_HI"}
|
||||
USB_NETS = {"USB_D_P", "USB_D_N", "FTDI_D_P", "FTDI_D_N"}
|
||||
CRITICAL_NETS = RF_NETS | USB_NETS
|
||||
|
||||
|
||||
def mm(value: float) -> int:
|
||||
return pcbnew.FromMM(value)
|
||||
|
||||
|
||||
def point(x: float, y: float) -> pcbnew.VECTOR2I:
|
||||
return pcbnew.VECTOR2I_MM(x, y)
|
||||
|
||||
|
||||
def footprint(board: pcbnew.BOARD, ref: str) -> pcbnew.FOOTPRINT:
|
||||
item = board.FindFootprintByReference(ref)
|
||||
if item is None:
|
||||
raise RuntimeError(f"Missing footprint {ref}")
|
||||
return item
|
||||
|
||||
|
||||
def pads(board: pcbnew.BOARD, ref: str, number: str) -> list[pcbnew.PAD]:
|
||||
result = [pad for pad in footprint(board, ref).Pads() if pad.GetNumber() == str(number)]
|
||||
if not result:
|
||||
raise RuntimeError(f"Missing pad {ref}.{number}")
|
||||
return result
|
||||
|
||||
|
||||
def pad_xy(board: pcbnew.BOARD, ref: str, number: str, index: int = 0) -> tuple[float, float]:
|
||||
position = pads(board, ref, number)[index].GetPosition()
|
||||
return pcbnew.ToMM(position.x), pcbnew.ToMM(position.y)
|
||||
|
||||
|
||||
def assert_ref_nets(board: pcbnew.BOARD, ref: str, expected: set[str]) -> None:
|
||||
actual = {pad.GetNetname() for pad in footprint(board, ref).Pads() if pad.GetNetname()}
|
||||
if actual != expected:
|
||||
raise RuntimeError(f"{ref} net mismatch: expected {sorted(expected)}, got {sorted(actual)}")
|
||||
|
||||
|
||||
def add_segment(board: pcbnew.BOARD, net_name: str,
|
||||
start: tuple[float, float], end: tuple[float, float],
|
||||
width: float, layer: int = pcbnew.F_Cu, locked: bool = True) -> 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(locked)
|
||||
board.Add(track)
|
||||
|
||||
|
||||
def add_polyline(board: pcbnew.BOARD, net_name: str,
|
||||
vertices: list[tuple[float, float]], width: float,
|
||||
layer: int = pcbnew.F_Cu) -> None:
|
||||
for start, end in zip(vertices, vertices[1:]):
|
||||
add_segment(board, net_name, start, end, width, layer)
|
||||
|
||||
|
||||
def add_via(board: pcbnew.BOARD, net_name: str, position: tuple[float, float],
|
||||
diameter: float = 0.45, drill: float = 0.225) -> None:
|
||||
via = pcbnew.PCB_VIA(board)
|
||||
via.SetPosition(point(*position))
|
||||
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 remove_critical_routes(board: pcbnew.BOARD) -> int:
|
||||
doomed = [item for item in board.GetTracks() if item.GetNetname() in CRITICAL_NETS]
|
||||
for item in doomed:
|
||||
board.Remove(item)
|
||||
return len(doomed)
|
||||
|
||||
|
||||
def remove_temporary_keepouts(board: pcbnew.BOARD) -> int:
|
||||
doomed = [zone for zone in board.Zones()
|
||||
if zone.GetIsRuleArea() and zone.GetZoneName().startswith("TEMP_")]
|
||||
for zone in doomed:
|
||||
board.Remove(zone)
|
||||
return len(doomed)
|
||||
|
||||
|
||||
def route_rf(board: pcbnew.BOARD) -> None:
|
||||
# 0.164-mm L1 CPWG with 0.200-mm ground gap: 49.65 ohms on the exact
|
||||
# JLC08161H-3313 L1/L2 dielectric construction.
|
||||
for ref, expected in {
|
||||
"C83": {"RF_SMA", "RF_PRE_SAW"},
|
||||
"C84": {"RF_POST_SAW", "GND"},
|
||||
"R39": {"RF_POST_SAW", "MAX_MIXIN_HI"},
|
||||
"C85": {"MAX_MIXIN_HI", "GND"},
|
||||
}.items():
|
||||
assert_ref_nets(board, ref, expected)
|
||||
|
||||
w = 0.164
|
||||
j2 = pad_xy(board, "J2", "1")
|
||||
dc_block_1 = pad_xy(board, "C83", "1")
|
||||
l4_2 = pad_xy(board, "L4", "2")
|
||||
add_polyline(board, "RF_SMA", [j2, l4_2, dc_block_1], w)
|
||||
d1 = pad_xy(board, "D1", "1")
|
||||
add_polyline(board, "RF_SMA", [d1, (d1[0], 45.07)], w)
|
||||
|
||||
add_polyline(board, "RF_PRE_SAW",
|
||||
[pad_xy(board, "C83", "2"), pad_xy(board, "FL1", "1")], w)
|
||||
|
||||
fl_out = pad_xy(board, "FL1", "4")
|
||||
tune1 = pad_xy(board, "C84", "1")
|
||||
series_1 = pad_xy(board, "R39", "1")
|
||||
add_polyline(board, "RF_POST_SAW", [fl_out, tune1, series_1], w)
|
||||
|
||||
series_2 = pad_xy(board, "R39", "2")
|
||||
mixin = pad_xy(board, "U21", "25")
|
||||
add_polyline(board, "MAX_MIXIN_HI",
|
||||
[series_2, (159.69, 41.80), (mixin[0], 41.80), mixin], w)
|
||||
tune2 = pad_xy(board, "C85", "1")
|
||||
add_polyline(board, "MAX_MIXIN_HI", [tune2, (tune2[0], 41.80)], w)
|
||||
|
||||
|
||||
def route_usb(board: pcbnew.BOARD) -> None:
|
||||
# Main device-side pair uses the upper receptacle contact pair. The two
|
||||
# mirrored USB-C contacts join through a sub-1.5-mm L3 crossover so neither
|
||||
# polarity creates a same-layer crossing at the receptacle.
|
||||
w = 0.135
|
||||
dm_main = pad_xy(board, "J1", "B7")
|
||||
dp_main = pad_xy(board, "J1", "A6")
|
||||
dm_mirror = pad_xy(board, "J1", "A7")
|
||||
dp_mirror = pad_xy(board, "J1", "B6")
|
||||
dm_join, dp_join = (28.30, 47.25), (28.80, 47.75)
|
||||
dm_aux, dp_aux = (27.30, 48.25), (27.80, 48.75)
|
||||
|
||||
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_N", [dm_mirror, dm_aux], w)
|
||||
add_polyline(board, "USB_D_P", [dp_mirror, dp_aux], w)
|
||||
for net, location in (("USB_D_N", dm_join), ("USB_D_P", dp_join),
|
||||
("USB_D_N", dm_aux), ("USB_D_P", dp_aux)):
|
||||
add_via(board, net, location)
|
||||
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)
|
||||
|
||||
dm_esd = pad_xy(board, "U6", "3")
|
||||
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",
|
||||
[dm_esd, (31.00, 51.40), (34.00, 54.40), (35.80, 54.40), dm_r], w)
|
||||
add_polyline(board, "USB_D_P",
|
||||
[dp_esd, (30.715, 51.685), (33.715, 54.685), (35.80, 54.685), dp_r], w)
|
||||
|
||||
# Equal 1.511-mm post-termination legs into the FT4232H USB PHY.
|
||||
add_polyline(board, "FTDI_D_N", [pad_xy(board, "R64", "2"), pad_xy(board, "U2", "7")], w)
|
||||
add_polyline(board, "FTDI_D_P", [pad_xy(board, "R63", "2"), pad_xy(board, "U2", "8")], w)
|
||||
|
||||
|
||||
def add_rf_ground(board: pcbnew.BOARD) -> None:
|
||||
zone = pcbnew.ZONE(board)
|
||||
zone.SetLayer(pcbnew.F_Cu)
|
||||
zone.SetNet(board.FindNet("GND"))
|
||||
zone.SetLocalClearance(mm(0.20))
|
||||
zone.SetMinThickness(mm(0.10))
|
||||
zone.SetPadConnection(pcbnew.ZONE_CONNECTION_FULL)
|
||||
zone.SetIslandRemovalMode(pcbnew.ISLAND_REMOVAL_MODE_NEVER)
|
||||
zone.SetAssignedPriority(100)
|
||||
outline = zone.Outline()
|
||||
outline.NewOutline()
|
||||
for x, y in ((152.0, 36.0), (174.5, 36.0), (174.5, 49.5), (152.0, 49.5)):
|
||||
outline.Append(point(x, y))
|
||||
board.Add(zone)
|
||||
|
||||
# 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.
|
||||
fence = [(x, 38.7) for x in range(153, 174, 2)]
|
||||
fence += [(x, 45.0) for x in range(161, 174, 2)]
|
||||
for location in fence:
|
||||
add_via(board, "GND", location, 0.45, 0.225)
|
||||
|
||||
|
||||
def routed_lengths(board: pcbnew.BOARD) -> dict[str, float]:
|
||||
totals = {net: 0.0 for net in CRITICAL_NETS}
|
||||
for item in board.GetTracks():
|
||||
net = item.GetNetname()
|
||||
if net not in totals or isinstance(item, pcbnew.PCB_VIA):
|
||||
continue
|
||||
start, end = item.GetStart(), item.GetEnd()
|
||||
totals[net] += math.hypot(pcbnew.ToMM(end.x - start.x), pcbnew.ToMM(end.y - start.y))
|
||||
return totals
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("session", type=Path, help="Freerouting .ses file")
|
||||
parser.add_argument("--output", type=Path, default=ROUTED)
|
||||
args = parser.parse_args()
|
||||
|
||||
board = pcbnew.LoadBoard(str(PLACED))
|
||||
if not pcbnew.ImportSpecctraSES(board, str(args.session)):
|
||||
raise RuntimeError(f"Could not import {args.session}")
|
||||
removed = remove_critical_routes(board)
|
||||
removed_keepouts = remove_temporary_keepouts(board)
|
||||
route_rf(board)
|
||||
route_usb(board)
|
||||
add_rf_ground(board)
|
||||
if not pcbnew.ZONE_FILLER(board).Fill(board.Zones()):
|
||||
raise RuntimeError("Final zone fill failed")
|
||||
pcbnew.SaveBoard(str(args.output), board)
|
||||
for suffix in (".kicad_pro", ".kicad_dru"):
|
||||
source = PLACED.with_suffix(suffix)
|
||||
if source.exists():
|
||||
shutil.copyfile(source, args.output.with_suffix(suffix))
|
||||
print(f"Removed {removed} autorouted critical-net items")
|
||||
print(f"Removed {removed_keepouts} temporary critical-route keepouts")
|
||||
print(f"Saved {args.output}")
|
||||
for net, length in sorted(routed_lengths(board).items()):
|
||||
print(f"{net:16s} {length:8.3f} mm")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+258
@@ -0,0 +1,258 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the authoritative Ti375 pin tables and MAX2771 register image.
|
||||
|
||||
The package source is exported directly from Efinity 2025.2's Ti375C529
|
||||
resource database by export_ti375_package.py. This script deliberately fails
|
||||
if a named ball, bank, voltage, or clock capability does not match that source.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
PACKAGE_CSV = ROOT / "docs" / "ti375_c529_package.csv"
|
||||
|
||||
|
||||
# signal, ball, direction, subsystem, selected I/O standard, design note
|
||||
ASSIGNMENTS = [
|
||||
("HARD_TCK", "G10", "input", "hard_jtag", "3.3 V LVCMOS", "Dedicated hard TAP TCK; FT4232H interface 1 BDBUS0 through power-domain isolation"),
|
||||
("HARD_TMS", "G12", "input", "hard_jtag", "3.3 V LVCMOS", "Dedicated hard TAP TMS; FT4232H interface 1 BDBUS3 through power-domain isolation"),
|
||||
("HARD_TDI", "G9", "input", "hard_jtag", "3.3 V LVCMOS", "Dedicated hard TAP TDI; FT4232H interface 1 BDBUS1 through power-domain isolation"),
|
||||
("HARD_TDO", "G13", "output", "hard_jtag", "3.3 V LVCMOS", "Dedicated hard TAP TDO; FT4232H interface 1 BDBUS2 through power-domain isolation"),
|
||||
("FPGA_CRESET_N", "F9", "input", "configuration", "1.8 V LVCMOS", "Open-drain fan-in of all-rail-valid, FTDI, and filtered external/manual reset requests"),
|
||||
("FPGA_CDONE", "F8", "output", "configuration", "1.8 V LVCMOS", "Configuration status test point"),
|
||||
("QSPI_CS_N", "A7", "output", "configuration", "1.8 V LVCMOS", "SPI-active SSL_N"),
|
||||
("QSPI_SCLK", "G6", "output", "configuration", "1.8 V LVCMOS", "SPI-active CCK; official CCK_PLLIN0 pin"),
|
||||
("QSPI_IO0", "H4", "bidirectional", "configuration", "1.8 V LVCMOS", "SPI-active CDI0"),
|
||||
("QSPI_IO1", "H5", "bidirectional", "configuration", "1.8 V LVCMOS", "SPI-active CDI1"),
|
||||
("QSPI_IO2", "G5", "bidirectional", "configuration", "1.8 V LVCMOS", "SPI-active CDI2"),
|
||||
("QSPI_IO3", "B7", "bidirectional", "configuration", "1.8 V LVCMOS", "SPI-active CDI3"),
|
||||
("FPGA_TEST_N", "J2", "input", "configuration", "1.8 V LVCMOS", "Mandatory external weak pull-up disables test mode during configuration"),
|
||||
("FPGA_CSI", "J5", "input", "configuration", "1.8 V LVCMOS", "Mandatory external weak pull-up selects this FPGA throughout configuration"),
|
||||
("RISCV_TCK", "F12", "input", "secondary_jtag", "3.3 V LVCMOS", "Fabric RISC-V JTAG clock moved off the plain-GPIO PMOD allocation"),
|
||||
("RISCV_TDI", "V19", "input", "secondary_jtag", "3.3 V LVCMOS", "Official development-kit hardened RISC-V JTAG assignment"),
|
||||
("RISCV_TMS", "P18", "input", "secondary_jtag", "3.3 V LVCMOS", "Clock-capable fabric pin; official development-kit assignment"),
|
||||
("RISCV_TDO", "T18", "output", "secondary_jtag", "3.3 V LVCMOS", "Official development-kit hardened RISC-V JTAG assignment"),
|
||||
("MAX_CLKOUT", "B11", "input", "max2771", "3.0 V LVCMOS", "Source-synchronous ADC capture clock on dedicated clock-capable input"),
|
||||
("MAX_I0", "D10", "input", "max2771", "3.0 V LVCMOS", "ADC I bit 0"),
|
||||
("MAX_I1", "D11", "input", "max2771", "3.0 V LVCMOS", "ADC I bit 1; clock-capable pad retained in same BR0 bank"),
|
||||
("MAX_Q0", "E10", "input", "max2771", "3.0 V LVCMOS", "ADC Q bit 0; PLL-capable pad in same BR0 bank"),
|
||||
("MAX_Q1", "E11", "input", "max2771", "3.0 V LVCMOS", "ADC Q bit 1; PLL-capable pad in same BR0 bank"),
|
||||
("MAX_LD", "F11", "input", "max2771", "3.0 V LVCMOS", "PLL lock status"),
|
||||
("MAX_SCLK", "F16", "output", "max2771", "3.0 V LVCMOS", "Three-wire serial clock"),
|
||||
("MAX_CSN", "F17", "output", "max2771", "3.0 V LVCMOS", "Three-wire active-low chip select"),
|
||||
("MAX_SDATA", "G16", "bidirectional", "max2771", "3.0 V LVCMOS", "Single bidirectional three-wire serial data; not split MOSI/MISO"),
|
||||
("MAX_SHDN", "G17", "output", "max2771", "3.0 V LVCMOS", "MAX2771 hardware shutdown control"),
|
||||
("IMU_SCK", "A8", "output", "sch16t", "3.3 V LVCMOS", "SafeSPI mode-0 clock"),
|
||||
("IMU_MOSI", "A9", "output", "sch16t", "3.3 V LVCMOS", "SafeSPI controller-to-sensor data"),
|
||||
("IMU_MISO", "B9", "input", "sch16t", "3.3 V LVCMOS", "SafeSPI sensor-to-controller data"),
|
||||
("IMU_CS", "B10", "output", "sch16t", "3.3 V LVCMOS", "SafeSPI active-low chip select"),
|
||||
("IMU_DRY_SYNC", "A11", "bidirectional", "sch16t", "3.3 V LVCMOS", "Murata multiplexed Data Ready output / SYNC input"),
|
||||
("IMU_EXTRESN", "C10", "output", "sch16t", "3.3 V LVCMOS", "External active-low reset"),
|
||||
("CLK_40M_FPGA", "U17", "input", "clock", "3.3 V LVCMOS", "40 MHz oscillator branch on clock-capable pin"),
|
||||
("UART0_RX", "P17", "input", "ft4232", "3.3 V LVCMOS", "FT4232H channel C TXD to FPGA RX"),
|
||||
("UART0_TX", "R17", "output", "ft4232", "3.3 V LVCMOS", "FPGA TX to FT4232H channel C RXD"),
|
||||
("UART1_RX", "R18", "input", "ft4232", "3.3 V LVCMOS", "FT4232H channel D TXD to FPGA RX"),
|
||||
("UART1_TX", "T19", "output", "ft4232", "3.3 V LVCMOS", "FPGA TX to FT4232H channel D RXD"),
|
||||
("FTDI_DBG_GPIO", "U19", "bidirectional", "ft4232", "3.3 V LVCMOS", "Channel D debug GPIO/test point; PLL-capable"),
|
||||
("PMOD1_1", "U6", "bidirectional", "pmod_3v3", "3.3 V LVCMOS", "PMOD 1 pin 1; plain HVIO GPIO"),
|
||||
("PMOD1_2", "V4", "bidirectional", "pmod_3v3", "3.3 V LVCMOS", "PMOD 1 pin 2; plain HVIO GPIO"),
|
||||
("PMOD1_3", "A13", "bidirectional", "pmod_tr1", "3.3 V LVCMOS", "PMOD 1 pin 3"),
|
||||
("PMOD1_4", "C9", "bidirectional", "pmod_3v3", "3.3 V LVCMOS", "PMOD 1 pin 4; plain HVIO GPIO"),
|
||||
("PMOD1_7", "C12", "bidirectional", "pmod_tr1", "3.3 V LVCMOS", "PMOD 1 pin 7"),
|
||||
("PMOD1_8", "C13", "bidirectional", "pmod_tr1", "3.3 V LVCMOS", "PMOD 1 pin 8"),
|
||||
("PMOD1_9", "C14", "bidirectional", "pmod_tr1", "3.3 V LVCMOS", "PMOD 1 pin 9"),
|
||||
("PMOD1_10", "D12", "bidirectional", "pmod_tr1", "3.3 V LVCMOS", "PMOD 1 pin 10"),
|
||||
("PMOD2_1", "V20", "bidirectional", "pmod_3v3", "3.3 V LVCMOS", "PMOD 2 pin 1; plain HVIO GPIO"),
|
||||
("PMOD2_2", "D13", "bidirectional", "pmod_tr2", "3.3 V LVCMOS", "PMOD 2 pin 2"),
|
||||
("PMOD2_3", "E13", "bidirectional", "pmod_tr2", "3.3 V LVCMOS", "PMOD 2 pin 3"),
|
||||
("PMOD2_4", "E14", "bidirectional", "pmod_tr2", "3.3 V LVCMOS", "PMOD 2 pin 4"),
|
||||
("PMOD2_7", "E16", "bidirectional", "pmod_tr2", "3.3 V LVCMOS", "PMOD 2 pin 7"),
|
||||
("PMOD2_8", "F13", "bidirectional", "pmod_tr2", "3.3 V LVCMOS", "PMOD 2 pin 8"),
|
||||
("PMOD2_9", "E15", "bidirectional", "pmod_tr2", "3.3 V LVCMOS", "PMOD 2 pin 9"),
|
||||
("PMOD2_10", "F14", "bidirectional", "pmod_tr2", "3.3 V LVCMOS", "PMOD 2 pin 10"),
|
||||
# The two 1.8-V PMODs use plain HSIO pairs only. None of these pads has
|
||||
# a configuration, clock, PLL, or external-feedback alternate function.
|
||||
("PMOD3_1", "C20", "bidirectional", "pmod_2e", "1.8 V LVCMOS", "PMOD 3 pin 1; HSIO pair 51 P"),
|
||||
("PMOD3_2", "B20", "bidirectional", "pmod_2e", "1.8 V LVCMOS", "PMOD 3 pin 2; HSIO pair 51 N"),
|
||||
("PMOD3_3", "C17", "bidirectional", "pmod_2e", "1.8 V LVCMOS", "PMOD 3 pin 3; HSIO pair 52 P"),
|
||||
("PMOD3_4", "B17", "bidirectional", "pmod_2e", "1.8 V LVCMOS", "PMOD 3 pin 4; HSIO pair 52 N"),
|
||||
("PMOD3_7", "A16", "bidirectional", "pmod_2e", "1.8 V LVCMOS", "PMOD 3 pin 7; HSIO pair 53 P"),
|
||||
("PMOD3_8", "A17", "bidirectional", "pmod_2e", "1.8 V LVCMOS", "PMOD 3 pin 8; HSIO pair 53 N"),
|
||||
("PMOD3_9", "C19", "bidirectional", "pmod_2e", "1.8 V LVCMOS", "PMOD 3 pin 9; HSIO pair 54 P"),
|
||||
("PMOD3_10", "B19", "bidirectional", "pmod_2e", "1.8 V LVCMOS", "PMOD 3 pin 10; HSIO pair 54 N"),
|
||||
("PMOD4_1", "B14", "bidirectional", "pmod_2e", "1.8 V LVCMOS", "PMOD 4 pin 1; HSIO pair 55 P"),
|
||||
("PMOD4_2", "A14", "bidirectional", "pmod_2e", "1.8 V LVCMOS", "PMOD 4 pin 2; HSIO pair 55 N"),
|
||||
("PMOD4_3", "J18", "bidirectional", "pmod_2e", "1.8 V LVCMOS", "PMOD 4 pin 3; HSIO pair 56 P"),
|
||||
("PMOD4_4", "H18", "bidirectional", "pmod_2e", "1.8 V LVCMOS", "PMOD 4 pin 4; HSIO pair 56 N"),
|
||||
("PMOD4_7", "A18", "bidirectional", "pmod_2e", "1.8 V LVCMOS", "PMOD 4 pin 7; HSIO pair 57 P"),
|
||||
("PMOD4_8", "A19", "bidirectional", "pmod_2e", "1.8 V LVCMOS", "PMOD 4 pin 8; HSIO pair 57 N"),
|
||||
("PMOD4_9", "D16", "bidirectional", "pmod_2e", "1.8 V LVCMOS", "PMOD 4 pin 9; HSIO pair 58 P"),
|
||||
("PMOD4_10", "D17", "bidirectional", "pmod_2e", "1.8 V LVCMOS", "PMOD 4 pin 10; HSIO pair 58 N"),
|
||||
]
|
||||
|
||||
|
||||
EXPECTED_HARD_NAMES = {
|
||||
"G10": "TCK", "G12": "TMS", "G9": "TDI", "G13": "TDO",
|
||||
"F9": "CRESET_N", "F8": "CDONE", "C8": "JTAG_VCCIO_SEL",
|
||||
}
|
||||
|
||||
|
||||
def load_package() -> tuple[list[dict[str, str]], dict[str, dict[str, str]]]:
|
||||
with PACKAGE_CSV.open(newline="") as f:
|
||||
rows = list(csv.DictReader(f))
|
||||
assert len(rows) == 529, f"C529 package source has {len(rows)} rows, expected 529"
|
||||
by_ball = {r["ball"]: r for r in rows}
|
||||
assert len(by_ball) == 529
|
||||
for ball, name in EXPECTED_HARD_NAMES.items():
|
||||
assert by_ball[ball]["official_pad_name"] == name, (ball, by_ball[ball], name)
|
||||
return rows, by_ball
|
||||
|
||||
|
||||
def output_pin_tables(rows: list[dict[str, str]], by_ball: dict[str, dict[str, str]]) -> None:
|
||||
assigned: dict[str, dict[str, str]] = {}
|
||||
out_rows: list[dict[str, str]] = []
|
||||
for signal, ball, direction, subsystem, standard, note in ASSIGNMENTS:
|
||||
assert ball not in assigned, f"duplicate assignment at {ball}"
|
||||
pkg = by_ball[ball]
|
||||
if "clock" in note.lower() and "clock-capable" in note.lower():
|
||||
assert "CLOCK_CAPABLE" in pkg["special_capability"], (signal, ball, pkg)
|
||||
if standard.startswith("3.3"):
|
||||
assert "3.3" in pkg["allowed_vccio_v"], (signal, ball, pkg)
|
||||
if standard.startswith("3.0"):
|
||||
assert "3.0" in pkg["allowed_vccio_v"], (signal, ball, pkg)
|
||||
if standard.startswith("1.8"):
|
||||
assert "1.8" in pkg["allowed_vccio_v"], (signal, ball, pkg)
|
||||
item = {
|
||||
"signal": signal,
|
||||
"ball": ball,
|
||||
"official_pin_name": pkg["official_pad_name"],
|
||||
"die_bank": pkg["die_bank"],
|
||||
"package_bank": pkg["package_bank"],
|
||||
"vccio": standard.split()[0] + " V",
|
||||
"io_standard": standard,
|
||||
"direction": direction,
|
||||
"clock_capability": "yes" if "CLOCK_CAPABLE" in pkg["special_capability"] else "no",
|
||||
"special_function": pkg["special_capability"],
|
||||
"subsystem": subsystem,
|
||||
"connector": "on-board",
|
||||
"connector_pin": "",
|
||||
"note": note,
|
||||
}
|
||||
if subsystem.startswith("pmod_"):
|
||||
pmod_number = signal.removeprefix("PMOD").split("_", 1)[0]
|
||||
item["connector"] = f"JPMOD{pmod_number}"
|
||||
item["connector_pin"] = signal.rsplit("_", 1)[1]
|
||||
assigned[ball] = item
|
||||
out_rows.append(item)
|
||||
|
||||
# Only the four PMODs are exposed. All other fabric GPIOs remain physically
|
||||
# unconnected, reducing connector count, board area, and escape-routing load.
|
||||
inventory: list[dict[str, str]] = []
|
||||
for pkg in rows:
|
||||
if pkg["fabric_gpio"] != "yes":
|
||||
continue
|
||||
ball = pkg["ball"]
|
||||
if ball in assigned:
|
||||
a = assigned[ball]
|
||||
inventory.append({
|
||||
"ball": ball, "official_pin_name": pkg["official_pad_name"],
|
||||
"package_bank": pkg["package_bank"], "bank_class": pkg["bank_class"],
|
||||
"selected_vccio": a["vccio"], "io_standard": a["io_standard"],
|
||||
"clock_capability": a["clock_capability"], "special_function": pkg["special_capability"],
|
||||
"status": "used", "signal": a["signal"], "connector": a["connector"],
|
||||
"connector_pin": a["connector_pin"], "note": a["note"],
|
||||
})
|
||||
continue
|
||||
voltage = "3.3 V" if pkg["bank_class"] == "HVIO" else "1.8 V"
|
||||
standard = "3.3 V LVCMOS" if pkg["bank_class"] == "HVIO" else "1.8 V LVCMOS"
|
||||
inventory.append({
|
||||
"ball": ball, "official_pin_name": pkg["official_pad_name"],
|
||||
"package_bank": pkg["package_bank"], "bank_class": pkg["bank_class"],
|
||||
"selected_vccio": voltage, "io_standard": standard,
|
||||
"clock_capability": "yes" if "CLOCK_CAPABLE" in pkg["special_capability"] else "no",
|
||||
"special_function": pkg["special_capability"],
|
||||
"status": "unused", "signal": "", "connector": "",
|
||||
"connector_pin": "", "note": "Unused fabric GPIO; left unconnected at the BGA",
|
||||
})
|
||||
|
||||
all_gpio = {r["ball"] for r in rows if r["fabric_gpio"] == "yes"}
|
||||
assert len(all_gpio) == 227
|
||||
used_gpio = set(assigned) & all_gpio
|
||||
# Two former stand-alone auxiliary probes were deliberately released when
|
||||
# the connector-like test-point bank was removed.
|
||||
assert len(used_gpio) == 66
|
||||
assert len(all_gpio - used_gpio) == 161
|
||||
|
||||
fieldnames = list(out_rows[0])
|
||||
with (ROOT / "docs" / "ti375_pinout.csv").open("w", newline="") as f:
|
||||
w = csv.DictWriter(f, fieldnames=fieldnames)
|
||||
w.writeheader()
|
||||
w.writerows(sorted(out_rows, key=lambda r: r["ball"]))
|
||||
with (ROOT / "docs" / "unused_gpio_inventory.csv").open("w", newline="") as f:
|
||||
w = csv.DictWriter(f, fieldnames=list(inventory[0]))
|
||||
w.writeheader()
|
||||
w.writerows(sorted(inventory, key=lambda r: r["ball"]))
|
||||
|
||||
with (ROOT / "docs" / "ti375_pin_constraints.isf").open("w") as f:
|
||||
f.write("# Efinity interface constraints generated from the official Ti375C529 database.\n")
|
||||
f.write("# Verify port names against the final RTL top level before importing.\n")
|
||||
for a in sorted(out_rows, key=lambda r: r["signal"]):
|
||||
f.write(f'set_pin_assignment -pin_name {a["ball"]} -port_name {a["signal"]}\n')
|
||||
|
||||
|
||||
def output_max2771_registers() -> None:
|
||||
defaults = {
|
||||
0x0: 0xBEA41603, 0x1: 0x20550288, 0x2: 0x0EAFA1DC,
|
||||
0x3: 0x698C0008, 0x4: 0x00C00080, 0x5: 0x08000070,
|
||||
0x6: 0x08000000, 0x7: 0x010061B2, 0x8: 0x01E0F401,
|
||||
0x9: 0x00C00002, 0xA: 0x010061B0,
|
||||
}
|
||||
regs = dict(defaults)
|
||||
# GPS L1/L1C wideband example from MAX2771 Rev 2 Applications Information.
|
||||
regs[0x0] = (regs[0x0] & ~(1 << 17)) | (1 << 17) # MIXPOLE=36 MHz
|
||||
regs[0x0] = (regs[0x0] & ~(0x3 << 15)) | (0x2 << 15) # both LNAs disabled
|
||||
regs[0x0] = (regs[0x0] & ~(0x3 << 13)) | (0x0 << 13) # high-band mixer
|
||||
regs[0x0] = (regs[0x0] & ~(0x7 << 3)) | (0x3 << 3) # 23.4 MHz LPF
|
||||
regs[0x0] &= ~(1 << 2) # 5th order
|
||||
regs[0x0] &= ~(1 << 1) # low-pass, not polyphase
|
||||
regs[0x3] |= 1 << 3 # integer-N PLL
|
||||
regs[0x4] = (regs[0x4] & ~((0x7FFF << 13) | (0x3FF << 3))) | (1540 << 13) | (16 << 3)
|
||||
regs[0x7] |= 1 << 28 # external ADC clock on ADC_CLKIN
|
||||
regs[0xA] |= 1 << 2 # CLKOUT outputs ADC sampling clock
|
||||
assert (regs[0] >> 22) & 0xFF == 0xFA # required reserved field
|
||||
assert (regs[9] >> 22) & 0x7 == 0x3 # required reserved field
|
||||
assert regs[4] == 0x00C08080
|
||||
assert abs(16.368 / 16 * 1540 - 1575.42) < 1e-9
|
||||
|
||||
with (ROOT / "docs" / "max2771_registers.csv").open("w", newline="") as f:
|
||||
fieldnames = ["address", "value_hex", "default_hex", "changed", "purpose"]
|
||||
w = csv.DictWriter(f, fieldnames=fieldnames)
|
||||
w.writeheader()
|
||||
purposes = {
|
||||
0: "MIXIN_HI, both LNAs disabled, 23.4 MHz 5th-order LPF, 36 MHz mixer pole",
|
||||
3: "Integer-N L1 PLL",
|
||||
4: "NDIV=1540, RDIV=16: 16.368 MHz / 16 * 1540 = 1575.42 MHz",
|
||||
7: "External 40 MHz ADC sampling clock on ADC_CLKIN",
|
||||
10: "Route ADC sampling clock to CLKOUT for source-synchronous FPGA capture",
|
||||
}
|
||||
for addr in range(11):
|
||||
w.writerow({
|
||||
"address": f"0x{addr:X}", "value_hex": f"0x{regs[addr]:08X}",
|
||||
"default_hex": f"0x{defaults[addr]:08X}",
|
||||
"changed": "yes" if regs[addr] != defaults[addr] else "no",
|
||||
"purpose": purposes.get(addr, "Preserve MAX2771 documented reset/default value"),
|
||||
})
|
||||
|
||||
|
||||
def main() -> None:
|
||||
rows, by_ball = load_package()
|
||||
output_pin_tables(rows, by_ball)
|
||||
output_max2771_registers()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,300 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate manufacturer-recommended custom footprints used by board.kicad_sch.
|
||||
|
||||
Dimensions are in millimetres and come from the package/land-pattern drawings
|
||||
listed in docs/reference_manifest.md. No generic IPC generator is used for
|
||||
the irregular high-current and sensor packages.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OUT = ROOT / "hardware" / "board.pretty"
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def fmt(value: float) -> str:
|
||||
return f"{value:.4f}".rstrip("0").rstrip(".") or "0"
|
||||
|
||||
|
||||
def header(name: str, description: str, courtyard: tuple[float, float], body: tuple[float, float]) -> list[str]:
|
||||
cw, ch = courtyard
|
||||
bw, bh = body
|
||||
return [
|
||||
f'(footprint "{name}"',
|
||||
' (version 20240108)',
|
||||
' (generator "board-footprints")',
|
||||
' (layer "F.Cu")',
|
||||
f' (descr "{description}")',
|
||||
' (attr smd)',
|
||||
' (property "Reference" "REF**" (at 0 -5 0) (layer "F.SilkS")',
|
||||
' (effects (font (size 1 1) (thickness 0.15))))',
|
||||
' (property "Value" "" (at 0 5 0) (layer "F.Fab") hide',
|
||||
' (effects (font (size 1 1) (thickness 0.15))))',
|
||||
f' (fp_rect (start {fmt(-bw/2)} {fmt(-bh/2)}) (end {fmt(bw/2)} {fmt(bh/2)})',
|
||||
' (stroke (width 0.1) (type default)) (fill none) (layer "F.Fab"))',
|
||||
f' (fp_rect (start {fmt(-cw/2)} {fmt(-ch/2)}) (end {fmt(cw/2)} {fmt(ch/2)})',
|
||||
' (stroke (width 0.05) (type default)) (fill none) (layer "F.CrtYd"))',
|
||||
]
|
||||
|
||||
|
||||
def silk_box(lines: list[str], width: float, height: float, mark: tuple[float, float] | None = None) -> None:
|
||||
x, y = width / 2, height / 2
|
||||
for x1, y1, x2, y2 in ((-x, -y, x, -y), (x, -y, x, y), (x, y, -x, y), (-x, y, -x, -y)):
|
||||
lines.append(
|
||||
f' (fp_line (start {fmt(x1)} {fmt(y1)}) (end {fmt(x2)} {fmt(y2)}) '
|
||||
'(stroke (width 0.15) (type default)) (layer "F.SilkS"))'
|
||||
)
|
||||
if mark:
|
||||
lines.append(
|
||||
f' (fp_circle (center {fmt(mark[0])} {fmt(mark[1])}) (end {fmt(mark[0]+0.25)} {fmt(mark[1])}) '
|
||||
'(stroke (width 0.15) (type default)) (fill solid) (layer "F.SilkS"))'
|
||||
)
|
||||
|
||||
|
||||
def pad(lines: list[str], number: str, x: float, y: float, sx: float, sy: float,
|
||||
shape: str = "roundrect", layers: str = '"F.Cu" "F.Paste" "F.Mask"',
|
||||
rotation: float = 0.0) -> None:
|
||||
rot = f" {fmt(rotation)}" if rotation else ""
|
||||
lines.append(
|
||||
f' (pad "{number}" smd {shape} (at {fmt(x)} {fmt(y)}{rot}) '
|
||||
f'(size {fmt(sx)} {fmt(sy)}) (layers {layers})'
|
||||
+ (' (roundrect_rratio 0.15)' if shape == "roundrect" else '') + ')'
|
||||
)
|
||||
|
||||
|
||||
def finish(name: str, lines: list[str]) -> None:
|
||||
lines.append(' (embedded_fonts no)')
|
||||
lines.append(')')
|
||||
(OUT / f"{name}.kicad_mod").write_text("\n".join(lines) + "\n")
|
||||
|
||||
|
||||
def texas_rpw0010a() -> None:
|
||||
name = "Texas_RPW0010A_QFN10"
|
||||
l = header(name, "TI RPW0010A VQFN-HR; TI land pattern 4225183/A", (3.0, 3.0), (2.0, 2.0))
|
||||
silk_box(l, 2.35, 2.35, (-1.45, -1.15))
|
||||
# Corner lands are the union of a 0.25 x 0.60 vertical land and a
|
||||
# 0.60 x 0.30 horizontal land, exactly matching TI's L-shaped examples.
|
||||
for number, sx, sy in (("1", -1, -1), ("4", -1, 1), ("7", 1, 1), ("10", 1, -1)):
|
||||
pad(l, number, sx * 0.725, sy * 0.9, 0.25, 0.60)
|
||||
pad(l, number, sx * 0.9, sy * 0.75, 0.60, 0.30)
|
||||
pad(l, "2", -0.9, -0.225, 0.60, 0.25)
|
||||
pad(l, "3", -0.9, 0.225, 0.60, 0.25)
|
||||
pad(l, "5", -0.25, 0, 0.30, 2.40)
|
||||
pad(l, "6", 0.25, 0, 0.30, 2.40)
|
||||
pad(l, "8", 0.9, 0.225, 0.60, 0.25)
|
||||
pad(l, "9", 0.9, -0.225, 0.60, 0.25)
|
||||
finish(name, l)
|
||||
|
||||
|
||||
def texas_rux0012a() -> None:
|
||||
name = "Texas_RUX0012A_VQFN12"
|
||||
l = header(name, "TI RUX0012A VQFN-HR; TI land pattern 4224010/A", (3.4, 3.4), (2.5, 2.5))
|
||||
silk_box(l, 2.65, 2.65, (-1.45, -1.2))
|
||||
# Large current pads, left pins 1/2 and right pins 8/7.
|
||||
for number, x, y in (("1", -0.675, -0.35), ("2", -0.675, 0.35),
|
||||
("8", 0.675, -0.35), ("7", 0.675, 0.35)):
|
||||
pad(l, number, x, y, 1.05, 0.40)
|
||||
for number, x in zip(("12", "11", "10", "9"), (-0.75, -0.25, 0.25, 0.75)):
|
||||
pad(l, number, x, -1.15, 0.20, 0.60)
|
||||
for number, x in zip(("3", "4", "5", "6"), (-0.75, -0.25, 0.25, 0.75)):
|
||||
pad(l, number, x, 1.15, 0.20, 0.60)
|
||||
finish(name, l)
|
||||
|
||||
|
||||
def texas_rqy0009a() -> None:
|
||||
name = "Texas_RQY0009A_VQFN9"
|
||||
l = header(name, "TI RQY0009A VQFN-HR; TI land pattern 4225639/A", (2.8, 3.2), (1.5, 2.0))
|
||||
silk_box(l, 1.75, 2.45, (-1.05, -1.25))
|
||||
for number, x in zip(("1", "9", "8"), (-0.5, 0, 0.5)):
|
||||
pad(l, number, x, -1.175, 0.25, 0.55)
|
||||
for number, x in zip(("4", "5", "6"), (-0.5, 0, 0.5)):
|
||||
pad(l, number, x, 1.175, 0.25, 0.55)
|
||||
pad(l, "2", -0.475, -0.40, 0.95, 0.20)
|
||||
pad(l, "3", -0.475, 0.40, 0.95, 0.20)
|
||||
pad(l, "7", 0.475, 0, 0.95, 0.20)
|
||||
finish(name, l)
|
||||
|
||||
|
||||
def texas_rzv0024a() -> None:
|
||||
name = "Texas_RZV0024A_WQFN24"
|
||||
l = header(name, "TI RZV0024A WQFN-FCRLF; TI land pattern 4228538/A", (3.5, 4.5), (3.05, 4.05))
|
||||
silk_box(l, 3.15, 4.15, (-1.75, -2.1))
|
||||
# Left pins 1..8 and right pins 20..13. The four corner pins use TI's
|
||||
# L-shaped lands; repeated pad numbers create one electrical pad in KiCad.
|
||||
for number, sx, sy in (("1", -1, -1), ("8", -1, 1), ("13", 1, 1), ("20", 1, -1)):
|
||||
# Trim only the two inward tips from the TI nominal 0.60-mm L shape.
|
||||
# This keeps the outer solder fillet while meeting JLC's 0.10-mm
|
||||
# copper web to the two adjacent lands.
|
||||
pad(l, number, sx * 1.25, sy * 1.825, 0.25, 0.45)
|
||||
pad(l, number, sx * 1.325, sy * 1.75, 0.45, 0.25)
|
||||
for number, y in zip(("2", "3", "4", "5", "6", "7"), (-1.25, -0.75, -0.25, 0.25, 0.75, 1.25)):
|
||||
pad(l, number, -1.25, y, 0.60, 0.25)
|
||||
for number, y in zip(("19", "18", "17", "16", "15", "14"), (-1.25, -0.75, -0.25, 0.25, 0.75, 1.25)):
|
||||
pad(l, number, 1.25, y, 0.60, 0.25)
|
||||
for number, x in zip(("24", "23", "22", "21"), (-0.75, -0.25, 0.25, 0.75)):
|
||||
pad(l, number, x, -1.75, 0.25, 0.60)
|
||||
for number, x in zip(("9", "10", "11", "12"), (-0.75, -0.25, 0.25, 0.75)):
|
||||
pad(l, number, x, 1.75, 0.25, 0.60)
|
||||
# TI calls out three separate exposed-metal zones, all connected to GND.
|
||||
# Leave a JLC-process-compatible 0.10-mm minimum copper gap to the
|
||||
# perimeter lands while retaining the package's three-zone topology.
|
||||
zones = ((-0.425, -0.65, 0.60, 1.10), (0.425, -0.65, 0.60, 1.10), (0, 0.65, 1.45, 1.10))
|
||||
for x, y, sx, sy in zones:
|
||||
pad(l, "25", x, y, sx, sy, "rect", '"F.Cu" "F.Mask"')
|
||||
# 70-75% paste coverage, split so there are no large paste deposits.
|
||||
nx = 2 if sx > 1.0 else 1
|
||||
for ix in range(nx):
|
||||
px = x + (ix - (nx - 1) / 2) * (sx / nx)
|
||||
pad(l, "", px, y, sx / nx - 0.12, sy - 0.18, "roundrect", '"F.Paste"')
|
||||
finish(name, l)
|
||||
|
||||
|
||||
def simple_two_pad(name: str, description: str, body: tuple[float, float],
|
||||
centres: float, size: tuple[float, float], courtyard: tuple[float, float]) -> None:
|
||||
l = header(name, description, courtyard, body)
|
||||
silk_box(l, body[0] + 0.15, body[1] + 0.15, (-body[0] / 2 - 0.2, -body[1] / 2))
|
||||
pad(l, "1", -centres, 0, *size)
|
||||
pad(l, "2", centres, 0, *size)
|
||||
finish(name, l)
|
||||
|
||||
|
||||
def texas_dpy0002a() -> None:
|
||||
simple_two_pad("Texas_DPY0002A_X1SON", "TI DPY0002A X1SON; ESD501 official land", (1.0, 0.6),
|
||||
0.35, (0.30, 0.50), (1.4, 1.0))
|
||||
|
||||
|
||||
def murata_dfe201612e() -> None:
|
||||
simple_two_pad("Murata_DFE201612E", "Murata DFE201612E recommended land J(E)TE243A-0006", (2.0, 1.6),
|
||||
0.8, (0.80, 1.80), (2.8, 2.4))
|
||||
|
||||
|
||||
def coilcraft_xel4030() -> None:
|
||||
simple_two_pad("Coilcraft_XEL4030", "Coilcraft XEL4030 recommended land, March 2026", (4.0, 4.0),
|
||||
1.675, (0.98, 3.40), (5.0, 4.8))
|
||||
|
||||
|
||||
def kyocera_600l() -> None:
|
||||
simple_two_pad("Kyocera_600L_0402", "KYOCERA AVX 600L 0402 recommended land", (1.0, 0.5),
|
||||
0.65, (0.90, 0.70), (2.2, 1.2))
|
||||
|
||||
|
||||
def kyocera_sf14() -> None:
|
||||
name = "Kyocera_SF14_5pad"
|
||||
l = header(name, "KYOCERA SF14-1582M5UUS2 SAW; official five-pad land", (1.8, 1.5), (1.1, 0.9))
|
||||
silk_box(l, 1.2, 1.0, (-0.8, 0))
|
||||
for number, x, y in (("1", -0.5, 0), ("2", 0, 0.2875), ("3", 0.5, 0.2875),
|
||||
("4", 0.5, -0.2875), ("5", 0, -0.2875)):
|
||||
pad(l, number, x, y, 0.25, 0.325)
|
||||
finish(name, l)
|
||||
|
||||
|
||||
def epson_tg2016smn() -> None:
|
||||
name = "Epson_TG2016SMN"
|
||||
l = header(name, "Epson TG2016SMN official 2.0 x 1.6 mm land", (3.0, 2.6), (2.0, 1.6))
|
||||
silk_box(l, 2.15, 1.75, (-1.35, -0.85))
|
||||
for number, x, y in (("1", -0.825, -0.565), ("2", -0.825, 0.565),
|
||||
("3", 0.825, 0.565), ("4", 0.825, -0.565)):
|
||||
pad(l, number, x, y, 0.60, 0.75)
|
||||
finish(name, l)
|
||||
|
||||
|
||||
def murata_sch16t() -> None:
|
||||
name = "Murata_SCH16T_K01"
|
||||
l = header(name, "Murata SCH16T-K01-1 official PCB land pattern", (15.2, 12.8), (13.4, 11.8))
|
||||
silk_box(l, 13.55, 11.95, (-7.15, -5.45))
|
||||
ys = [-4.4 + 0.8 * i for i in range(12)]
|
||||
for number, y in zip(range(1, 13), ys):
|
||||
pad(l, str(number), -6.165, y, 1.87, 0.50)
|
||||
for number, y in zip(range(24, 12, -1), ys):
|
||||
pad(l, str(number), 6.165, y, 1.87, 0.50)
|
||||
# Assembly-critical inertial orientation marking.
|
||||
l += [
|
||||
' (fp_text user "X+ ->" (at 0 -2) (layer "F.SilkS") (effects (font (size 0.8 0.8) (thickness 0.14))))',
|
||||
' (fp_text user "Y+ ^" (at 0 0) (layer "F.SilkS") (effects (font (size 0.8 0.8) (thickness 0.14))))',
|
||||
]
|
||||
finish(name, l)
|
||||
|
||||
|
||||
def ftdi_vqfn56() -> None:
|
||||
name = "FTDI_VQFN56_EP5p9"
|
||||
l = header(name, "FTDI FT4232H-56Q VQFN56 land from TN_166; 5.9-mm EP", (9.2, 9.2), (8.0, 8.0))
|
||||
silk_box(l, 8.15, 8.15, (-4.4, -3.85))
|
||||
coords = [-3.25 + 0.5 * i for i in range(14)]
|
||||
for number, y in zip(range(1, 15), coords):
|
||||
pad(l, str(number), -4.00, y, 0.80, 0.25)
|
||||
for number, x in zip(range(15, 29), coords):
|
||||
pad(l, str(number), x, 4.00, 0.25, 0.80)
|
||||
for number, y in zip(range(29, 43), reversed(coords)):
|
||||
pad(l, str(number), 4.00, y, 0.80, 0.25)
|
||||
for number, x in zip(range(43, 57), reversed(coords)):
|
||||
pad(l, str(number), x, -4.00, 0.25, 0.80)
|
||||
pad(l, "57", 0, 0, 5.90, 5.90, "rect", '"F.Cu" "F.Mask"')
|
||||
for x in (-2.22, -0.74, 0.74, 2.22):
|
||||
for y in (-2.22, -0.74, 0.74, 2.22):
|
||||
pad(l, "", x, y, 1.20, 1.20, "roundrect", '"F.Paste"')
|
||||
finish(name, l)
|
||||
|
||||
|
||||
def amphenol_jtag() -> None:
|
||||
name = "Amphenol_20021121_00010C4LF"
|
||||
l = header(name, "Amphenol Minitek127 20021121-00010C4LF vertical SMT 2x5", (7.4, 6.0), (7.0, 4.6))
|
||||
silk_box(l, 7.0, 4.7, (-3.8, 2.3))
|
||||
xs = (-2.54, -1.27, 0, 1.27, 2.54)
|
||||
for col, x in enumerate(xs):
|
||||
pad(l, str(1 + col * 2), x, 1.95, 0.76, 2.40)
|
||||
pad(l, str(2 + col * 2), x, -1.95, 0.76, 2.40)
|
||||
finish(name, l)
|
||||
|
||||
|
||||
def digilent_pmod_2x6() -> None:
|
||||
"""2.54-mm dual-row PMOD with Digilent's row-wise pin numbering.
|
||||
|
||||
Generic KiCad 2xN headers use IDC odd/even numbering. A PMOD instead has
|
||||
pins 1..6 on one physical row and 7..12 on the other, so a dedicated
|
||||
footprint prevents a silent GND/VCC and signal-position mismatch.
|
||||
"""
|
||||
name = "Digilent_PMOD_2x6_P2.54mm"
|
||||
l = [
|
||||
f'(footprint "{name}"',
|
||||
' (version 20240108)',
|
||||
' (generator "board-footprints")',
|
||||
' (layer "F.Cu")',
|
||||
' (descr "Digilent PMOD 2x6, 2.54-mm pitch, row-wise pins 1-6 and 7-12")',
|
||||
' (attr through_hole)',
|
||||
' (property "Reference" "REF**" (at 6.35 -2.5 0) (layer "F.SilkS")',
|
||||
' (effects (font (size 1 1) (thickness 0.15))))',
|
||||
' (property "Value" "" (at 6.35 5 0) (layer "F.Fab") hide',
|
||||
' (effects (font (size 1 1) (thickness 0.15))))',
|
||||
' (fp_rect (start -1.27 -1.27) (end 13.97 3.81)',
|
||||
' (stroke (width 0.15) (type default)) (fill none) (layer "F.SilkS"))',
|
||||
' (fp_rect (start -1.5 -1.5) (end 14.2 4.04)',
|
||||
' (stroke (width 0.05) (type default)) (fill none) (layer "F.CrtYd"))',
|
||||
' (fp_circle (center -0.8 -0.8) (end -0.5 -0.8)',
|
||||
' (stroke (width 0.15) (type default)) (fill solid) (layer "F.SilkS"))',
|
||||
]
|
||||
for number in range(1, 13):
|
||||
row = 0 if number <= 6 else 1
|
||||
column = number - 1 if number <= 6 else number - 7
|
||||
shape = "rect" if number == 1 else "circle"
|
||||
l.append(
|
||||
f' (pad "{number}" thru_hole {shape} (at {fmt(column * 2.54)} {fmt(row * 2.54)}) '
|
||||
'(size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask"))'
|
||||
)
|
||||
finish(name, l)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
for generator in (texas_rpw0010a, texas_rux0012a, texas_rzv0024a, texas_rqy0009a,
|
||||
murata_dfe201612e, texas_dpy0002a, kyocera_600l, kyocera_sf14,
|
||||
epson_tg2016smn, murata_sch16t, ftdi_vqfn56,
|
||||
coilcraft_xel4030, amphenol_jtag, digilent_pmod_2x6):
|
||||
generator()
|
||||
print(f"Generated {len(list(OUT.glob('*.kicad_mod')))} custom footprints in {OUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,460 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the KiCad schematic hierarchy and BOM tables from design_data."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import copy
|
||||
import math
|
||||
import re
|
||||
import sys
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
from design_data import Component, Pin, components # noqa: E402
|
||||
|
||||
HW = ROOT / "hardware"
|
||||
DOCS = ROOT / "docs"
|
||||
OUT = ROOT / "outputs"
|
||||
HW.mkdir(exist_ok=True)
|
||||
DOCS.mkdir(exist_ok=True)
|
||||
OUT.mkdir(exist_ok=True)
|
||||
|
||||
|
||||
def q(text: str) -> str:
|
||||
return text.replace("\\", "\\\\").replace('"', '\\"')
|
||||
|
||||
|
||||
def safe(text: str) -> str:
|
||||
value = re.sub(r"[^A-Za-z0-9_]+", "_", text)
|
||||
return value.strip("_") or "PIN"
|
||||
|
||||
|
||||
def category(comp: Component) -> str:
|
||||
nets = {pin.net or "" for pin in comp.pins}
|
||||
if comp.ref == "U1" or any(n.startswith("REF_RES") for n in nets):
|
||||
return "fpga"
|
||||
if comp.ref in {"J2", "J5", "D1", "FL1", "U21", "U23", "U24", "U27"} or any(
|
||||
n.startswith(("RF_", "MAX_", "ANT_", "TCXO", "OSC40")) for n in nets):
|
||||
return "rf_clock"
|
||||
if comp.ref == "U22" or any(n.startswith("IMU_") for n in nets):
|
||||
return "imu"
|
||||
if comp.ref in {"U2", "U3", "U25"} or any(
|
||||
n.startswith(("FTDI_", "HARD_", "RISCV_", "UART")) for n in nets):
|
||||
return "usb_debug"
|
||||
if comp.ref == "U26" or comp.ref.startswith(("JPMOD", "JH", "JHV")) or any(
|
||||
n.startswith(("QSPI_", "EXP_", "PMOD")) for n in nets):
|
||||
return "configuration_expansion"
|
||||
return "power_usb"
|
||||
|
||||
|
||||
SHEETS = ["power_usb", "rf_clock", "imu", "usb_debug", "configuration_expansion", "fpga"]
|
||||
|
||||
|
||||
def chunks(comp: Component) -> list[list[Pin]]:
|
||||
if comp.ref == "U1":
|
||||
# Fourteen units keeps every unit below 40 balls and the complete 529-ball
|
||||
# package readable without losing the common U1 reference.
|
||||
size = math.ceil(len(comp.pins) / 14)
|
||||
return [comp.pins[i:i + size] for i in range(0, len(comp.pins), size)]
|
||||
return [comp.pins]
|
||||
|
||||
|
||||
def geometry(pins: list[Pin]) -> tuple[int, int, dict[str, tuple[int, int, str]]]:
|
||||
left = pins[::2]
|
||||
right = pins[1::2]
|
||||
rows = max(len(left), len(right), 2)
|
||||
half_h = max(150, (rows - 1) * 50 + 100)
|
||||
endpoints: dict[str, tuple[int, int, str]] = {}
|
||||
start_y = (len(left) - 1) * 50
|
||||
for idx, pin in enumerate(left):
|
||||
endpoints[pin.number] = (-700, start_y - idx * 100, "R")
|
||||
start_y = (len(right) - 1) * 50
|
||||
for idx, pin in enumerate(right):
|
||||
endpoints[pin.number] = (700, start_y - idx * 100, "L")
|
||||
return 500, half_h, endpoints
|
||||
|
||||
|
||||
def symbol_library() -> str:
|
||||
lines = ["EESchema-LIBRARY Version 2.4", "#encoding utf-8"]
|
||||
etypes = {
|
||||
"input": "I", "output": "O", "bidirectional": "B", "power_in": "W",
|
||||
"power_out": "w", "open_collector": "C", "passive": "P",
|
||||
}
|
||||
for comp in components:
|
||||
name = f"BOARD_{safe(comp.ref)}"
|
||||
units = chunks(comp)
|
||||
lines += ["#", f"# {name}", "#",
|
||||
f"DEF {name} {comp.ref[0]} 0 30 Y Y {len(units)} L N",
|
||||
f'F0 "{q(comp.ref)}" 0 200 50 H V C CNN',
|
||||
f'F1 "{q(comp.value)}" 0 100 50 H V C CNN',
|
||||
f'F2 "{q(comp.footprint)}" 0 -100 50 H I C CNN',
|
||||
f'F3 "" 0 0 50 H I C CNN', "DRAW"]
|
||||
for unit, pins in enumerate(units, 1):
|
||||
half_w, half_h, endpoints = geometry(pins)
|
||||
lines.append(f"S {-half_w} {half_h} {half_w} {-half_h} {unit} 1 10 f")
|
||||
for pin in pins:
|
||||
x, y, orient = endpoints[pin.number]
|
||||
lines.append(
|
||||
f"X {safe(pin.name)} {pin.number} {x} {y} 200 {orient} 30 30 {unit} 1 "
|
||||
f"{etypes.get(pin.etype, 'P')}"
|
||||
)
|
||||
lines += ["ENDDRAW", "ENDDEF"]
|
||||
lines += ["#", "#End Library"]
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def component_block(comp: Component, unit: int, x: int, y: int, stamp: int) -> list[str]:
|
||||
name = f"BOARD_{safe(comp.ref)}"
|
||||
f0y = y - 100
|
||||
return [
|
||||
"$Comp", f"L {name} {comp.ref}", f"U {unit} 1 {stamp:08X}", f"P {x} {y}",
|
||||
f'F 0 "{q(comp.ref)}" H {x} {f0y} 50 0000 C CNN',
|
||||
f'F 1 "{q(comp.value)}" H {x} {y + 100} 50 0000 C CNN',
|
||||
f'F 2 "{q(comp.footprint)}" H {x} {y - 200} 50 0001 C CNN',
|
||||
f'F 3 "" H {x} {y} 50 0001 C CNN',
|
||||
f'F 4 "{q(comp.mpn)}" H {x} {y} 50 0001 C CNN "MPN"',
|
||||
f'F 5 "{q(comp.manufacturer)}" H {x} {y} 50 0001 C CNN "Manufacturer"',
|
||||
f"\t{unit} {x} {y}", "\t1 0 0 -1", "$EndComp",
|
||||
]
|
||||
|
||||
|
||||
def add_pin_connections(lines: list[str], pins: list[Pin], x: int, y: int) -> None:
|
||||
_, _, endpoints = geometry(pins)
|
||||
for pin in pins:
|
||||
px, py, side = endpoints[pin.number]
|
||||
px += x
|
||||
py += y
|
||||
if pin.net is None:
|
||||
lines.append(f"NoConn ~ {px} {py}")
|
||||
continue
|
||||
wire_end = px - 150 if side == "R" else px + 150
|
||||
lines.append(f"Wire Wire Line\n\t{px} {py} {wire_end} {py}")
|
||||
orient = 2 if side == "R" else 0
|
||||
lines.append(f"Text GLabel {wire_end} {py} {orient} 30 BiDi ~ 0\n{pin.net}")
|
||||
|
||||
|
||||
def sheet_file(sheet: str, comps: list[Component], sheet_index: int) -> str:
|
||||
lines = [
|
||||
"EESchema Schematic File Version 4", "LIBS:board-cache", "EELAYER 29 0",
|
||||
"EELAYER END", "$Descr A0 46811 33110", "encoding utf-8", "Sheet 1 1",
|
||||
f'Title "Ti375 GNSS/IMU board - {sheet.replace("_", " ").title()}"',
|
||||
'Date "2026-08-15"', 'Rev "A0"', 'Comp "Open hardware design for JLCPCB"',
|
||||
'Comment1 "Generated from scripts/design_data.py; do not hand-edit"',
|
||||
'Comment2 "No LEDs, no push buttons"', 'Comment3 "Ti375C529C3 / MAX2771 / SCH16T-K01-1"',
|
||||
'Comment4 "JLC08161H-3313 eight-layer stack"', "$EndDescr",
|
||||
]
|
||||
x, y = 1800, 1800
|
||||
column_width = 3000
|
||||
bottom = 31600
|
||||
stamp = 0x65000000 + sheet_index * 0x10000
|
||||
for comp in comps:
|
||||
for unit, pins in enumerate(chunks(comp), 1):
|
||||
_, half_h, _ = geometry(pins)
|
||||
height = 2 * half_h + 500
|
||||
if y + half_h > bottom:
|
||||
x += column_width
|
||||
y = 1800
|
||||
if x > 45000:
|
||||
raise RuntimeError(f"{sheet} overflow at {comp.ref}")
|
||||
stamp += 1
|
||||
lines += component_block(comp, unit, x, y, stamp)
|
||||
add_pin_connections(lines, pins, x, y)
|
||||
y += height
|
||||
lines += ["$EndSCHEMATC"]
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def root_file() -> str:
|
||||
lines = [
|
||||
"EESchema Schematic File Version 4", "LIBS:board-cache", "EELAYER 29 0", "EELAYER END",
|
||||
"$Descr A4 11693 8268", "encoding utf-8", "Sheet 1 7",
|
||||
'Title "Ti375 C529 GNSS/IMU acquisition board"', 'Date "2026-08-15"', 'Rev "A0"',
|
||||
'Comp "JLCPCB manufacture/assembly package"',
|
||||
'Comment1 "USB-C 5 V primary; protected external 5 V; no back-feed"',
|
||||
'Comment2 "GPS L1/L1C direct MAX2771 MIXIN_HI; selectable active-antenna bias"',
|
||||
'Comment3 "Every Ti375 programmable GPIO has a documented disposition"',
|
||||
'Comment4 "Generated source - see docs/design_audit.md"', "$EndDescr",
|
||||
]
|
||||
for idx, sheet in enumerate(SHEETS, 1):
|
||||
x = 1400 + ((idx - 1) % 2) * 4800
|
||||
y = 1000 + ((idx - 1) // 2) * 1900
|
||||
lines += [
|
||||
"$Sheet", f"S {x} {y} 3600 1200", f"U {0x66000000 + idx:08X}",
|
||||
f'F0 "{sheet.replace("_", " ").upper()}" 60', f'F1 "{sheet}.sch" 60', "$EndSheet",
|
||||
]
|
||||
lines += ["$EndSCHEMATC"]
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def write_boms() -> None:
|
||||
fields = ["Reference", "Qty", "Value", "Manufacturer", "MPN", "Footprint",
|
||||
"Assembly", "DNP", "Description"]
|
||||
grouped: dict[tuple[str, ...], list[str]] = defaultdict(list)
|
||||
for comp in components:
|
||||
key = (comp.value, comp.manufacturer, comp.mpn, comp.footprint,
|
||||
comp.assembly, "yes" if comp.dnp else "no", comp.description)
|
||||
grouped[key].append(comp.ref)
|
||||
with (OUT / "bom.csv").open("w", newline="") as f:
|
||||
w = csv.DictWriter(f, fieldnames=fields)
|
||||
w.writeheader()
|
||||
for key, refs in sorted(grouped.items(), key=lambda item: item[1][0]):
|
||||
value, manufacturer, mpn, footprint, assembly, dnp, desc = key
|
||||
w.writerow(dict(zip(fields, [",".join(refs), len(refs), value, manufacturer,
|
||||
mpn, footprint, assembly, dnp, desc])))
|
||||
|
||||
jlc_fields = ["Comment", "Designator", "Footprint", "LCSC Part #", "Manufacturer",
|
||||
"Manufacturer Part Number", "Quantity", "Assembly Type", "DNP"]
|
||||
with (OUT / "jlcpcb_bom.csv").open("w", newline="") as f:
|
||||
w = csv.DictWriter(f, fieldnames=jlc_fields)
|
||||
w.writeheader()
|
||||
for key, refs in sorted(grouped.items(), key=lambda item: item[1][0]):
|
||||
value, manufacturer, mpn, footprint, assembly, dnp, _ = key
|
||||
w.writerow({"Comment": value, "Designator": ",".join(refs), "Footprint": footprint,
|
||||
"LCSC Part #": "", "Manufacturer": manufacturer,
|
||||
"Manufacturer Part Number": mpn, "Quantity": len(refs),
|
||||
"Assembly Type": assembly if not dnp == "yes" else "DNP", "DNP": dnp})
|
||||
|
||||
|
||||
def duid(text: str) -> str:
|
||||
"""Stable UUIDs keep generated KiCad diffs reproducible."""
|
||||
return str(uuid.uuid5(uuid.UUID("830005da-d201-4458-b73e-8946620d7012"), text))
|
||||
|
||||
|
||||
def write_native_schematic() -> None:
|
||||
"""Write a flat, native KiCad schematic using kiutils' format model.
|
||||
|
||||
Flat global-label connectivity is deliberate: it keeps 529 FPGA balls and
|
||||
273 physical components machine-auditable without hidden hierarchical pins.
|
||||
"""
|
||||
try:
|
||||
from kiutils.schematic import Schematic
|
||||
from kiutils.symbol import Symbol, SymbolLib, SymbolPin
|
||||
from kiutils.items.common import (Effects, Fill, Font, PageSettings,
|
||||
Position, Property, Stroke, TitleBlock)
|
||||
from kiutils.items.schitems import (GlobalLabel, HierarchicalSheetInstance,
|
||||
NoConnect, SchematicSymbol, SymbolInstance)
|
||||
from kiutils.items.syitems import SyRect
|
||||
except ImportError as exc:
|
||||
raise SystemExit("Install the pinned generator dependency: python3 -m pip install -r requirements.txt") from exc
|
||||
|
||||
sch = Schematic.create_new()
|
||||
sch.version = "20231120"
|
||||
sch.generator = "board_generator"
|
||||
sch.uuid = duid("schematic-root")
|
||||
sch.paper = PageSettings(paperSize="A0")
|
||||
sch.titleBlock = TitleBlock(
|
||||
title="Ti375 C529 GPS L1/L1C + SCH16T acquisition board",
|
||||
date="2026-08-15", revision="A0", company="JLCPCB manufacturing design",
|
||||
comments={1: "Generated from official-package connectivity in scripts/design_data.py",
|
||||
2: "USB-C 5 V primary / protected EXT_5V / no source back-feed",
|
||||
3: "MAX2771 RF connects only to MIXIN_HI; internal LNAs unused",
|
||||
4: "No LEDs and no push buttons"})
|
||||
# create_new supplies the root instance; make the path explicit and stable.
|
||||
sch.sheetInstances = [HierarchicalSheetInstance(instancePath="/", page="1")]
|
||||
|
||||
font = Font(width=0.8, height=0.8)
|
||||
hidden = Effects(font=font, hide=True)
|
||||
shown = Effects(font=font)
|
||||
etypes = {
|
||||
"input": "input", "output": "output", "bidirectional": "bidirectional",
|
||||
"power_in": "power_in", "power_out": "power_out",
|
||||
"open_collector": "open_collector", "passive": "passive",
|
||||
}
|
||||
|
||||
def ngeom(pins: list[Pin]):
|
||||
left, right = pins[::2], pins[1::2]
|
||||
rows = max(len(left), len(right), 2)
|
||||
half_h = max(3.81, (rows - 1) * 1.27 + 2.54)
|
||||
endpoints = {}
|
||||
for side_pins, x, angle in ((left, -17.78, 0), (right, 17.78, 180)):
|
||||
start_y = -(len(side_pins) - 1) * 1.27
|
||||
for idx, pin in enumerate(side_pins):
|
||||
endpoints[pin.number] = (x, start_y + idx * 2.54, angle)
|
||||
return 12.7, half_h, endpoints
|
||||
|
||||
lib_by_ref = {}
|
||||
for comp in components:
|
||||
symbol_name = f"BOARD_{safe(comp.ref)}"
|
||||
parent = Symbol.create_new(f"board:{symbol_name}", comp.ref[0], comp.value, comp.footprint)
|
||||
parent.pinNames = True
|
||||
parent.pinNamesOffset = 0.508
|
||||
parent.properties.extend([
|
||||
Property(key="MPN", value=comp.mpn, id=4, effects=hidden),
|
||||
Property(key="Manufacturer", value=comp.manufacturer, id=5, effects=hidden),
|
||||
Property(key="Description", value=comp.description, id=6, effects=hidden),
|
||||
])
|
||||
seen_power_out_nets: set[str] = set()
|
||||
for unit_idx, pins in enumerate(chunks(comp), 1):
|
||||
unit = Symbol()
|
||||
unit.libId = f"{symbol_name}_{unit_idx}_1"
|
||||
half_w, half_h, endpoints = ngeom(pins)
|
||||
unit.graphicItems.append(SyRect(start=Position(-half_w, -half_h),
|
||||
end=Position(half_w, half_h),
|
||||
stroke=Stroke(width=0.254, type="default"),
|
||||
fill=Fill(type="background")))
|
||||
for pin in pins:
|
||||
x, y, angle = endpoints[pin.number]
|
||||
electrical_type = etypes.get(pin.etype, "passive")
|
||||
if pin.net == "GND" and electrical_type == "bidirectional":
|
||||
# Deliberately strapped bidirectional configuration pins
|
||||
# (for example unused TPS6287B I2C) are passive straps in
|
||||
# this design, not active bus drivers.
|
||||
electrical_type = "passive"
|
||||
if electrical_type == "power_out" and pin.net in seen_power_out_nets:
|
||||
electrical_type = "passive"
|
||||
elif electrical_type == "power_out" and pin.net is not None:
|
||||
seen_power_out_nets.add(pin.net)
|
||||
unit.pins.append(SymbolPin(
|
||||
electricalType=electrical_type, graphicalStyle="line",
|
||||
position=Position(x, y, angle), length=5.08,
|
||||
name=pin.name, number=pin.number,
|
||||
nameEffects=Effects(font=Font(width=0.65, height=0.65)),
|
||||
numberEffects=Effects(font=Font(width=0.65, height=0.65))))
|
||||
parent.units.append(unit)
|
||||
sch.libSymbols.append(parent)
|
||||
lib_by_ref[comp.ref] = parent
|
||||
|
||||
# Pack tallest symbols first within functional groups, using generous label clearance.
|
||||
ordered = sorted(components, key=lambda comp: (SHEETS.index(category(comp)),
|
||||
-max(len(x) for x in chunks(comp)), comp.ref))
|
||||
# All origins and margins are exact multiples of KiCad's 1.27-mm
|
||||
# connection grid. Symbol-library Y coordinates are Cartesian (positive
|
||||
# up), whereas schematic sheet Y coordinates grow downward.
|
||||
x, y = 50.8, 38.1
|
||||
column_width, bottom = 127.0, 800.1
|
||||
for comp in ordered:
|
||||
for unit_idx, pins in enumerate(chunks(comp), 1):
|
||||
_, half_h, endpoints = ngeom(pins)
|
||||
height = 2 * half_h + 10.16
|
||||
if y + half_h > bottom:
|
||||
x += column_width
|
||||
y = 38.1
|
||||
if x > 1150.0:
|
||||
raise RuntimeError(f"native A0 sheet overflow at {comp.ref}")
|
||||
origin_y = y + half_h
|
||||
inst_uuid = duid(f"symbol:{comp.ref}:{unit_idx}")
|
||||
inst = SchematicSymbol()
|
||||
inst.libId = f"board:BOARD_{safe(comp.ref)}"
|
||||
inst.position = Position(x, origin_y, 0)
|
||||
inst.unit = unit_idx
|
||||
inst.inBom = not comp.dnp
|
||||
inst.onBoard = True
|
||||
inst.dnp = comp.dnp
|
||||
inst.uuid = inst_uuid
|
||||
inst.properties = [
|
||||
Property(key="Reference", value=comp.ref, id=0,
|
||||
position=Position(x, origin_y - half_h - 2.0, 0), effects=shown),
|
||||
Property(key="Value", value=comp.value, id=1,
|
||||
position=Position(x, origin_y + half_h + 2.0, 0), effects=shown),
|
||||
Property(key="Footprint", value=comp.footprint, id=2,
|
||||
position=Position(x, origin_y, 0), effects=hidden),
|
||||
Property(key="Datasheet", value="", id=3,
|
||||
position=Position(x, origin_y, 0), effects=hidden),
|
||||
Property(key="MPN", value=comp.mpn, id=4,
|
||||
position=Position(x, origin_y, 0), effects=hidden),
|
||||
Property(key="Manufacturer", value=comp.manufacturer, id=5,
|
||||
position=Position(x, origin_y, 0), effects=hidden),
|
||||
]
|
||||
for pin in pins:
|
||||
inst.pins[pin.number] = duid(f"pin:{comp.ref}:{unit_idx}:{pin.number}")
|
||||
px, py, angle = endpoints[pin.number]
|
||||
pos = Position(x + px, origin_y - py, angle)
|
||||
if pin.net is None:
|
||||
sch.noConnects.append(NoConnect(position=pos,
|
||||
uuid=duid(f"nc:{comp.ref}:{unit_idx}:{pin.number}")))
|
||||
else:
|
||||
sch.globalLabels.append(GlobalLabel(
|
||||
text=pin.net, shape="bidirectional", position=pos, effects=shown,
|
||||
uuid=duid(f"label:{comp.ref}:{unit_idx}:{pin.number}")))
|
||||
sch.schematicSymbols.append(inst)
|
||||
sch.symbolInstances.append(SymbolInstance(
|
||||
path=f"/{inst_uuid}", reference=comp.ref, unit=unit_idx,
|
||||
value=comp.value, footprint=comp.footprint))
|
||||
y += height
|
||||
|
||||
# ERC power flags are schematic-only assertions. They are generated only
|
||||
# for nets containing power-input pins but no direct power-output pin (for
|
||||
# example GND, external connector feeds, and ferrite-isolated subrails).
|
||||
power_inputs = {pin.net for comp in components for pin in comp.pins
|
||||
if pin.etype == "power_in" and pin.net is not None}
|
||||
power_outputs = {pin.net for comp in components for pin in comp.pins
|
||||
if pin.etype == "power_out" and pin.net is not None}
|
||||
flag_nets = sorted(power_inputs - power_outputs)
|
||||
flag_lib = Symbol.create_new("board:PWR_FLAG", "#FLG", "PWR_FLAG")
|
||||
flag_lib.inBom = False
|
||||
flag_lib.onBoard = False
|
||||
flag_lib.isPower = True
|
||||
flag_unit = Symbol()
|
||||
flag_unit.libId = "PWR_FLAG_1_1"
|
||||
flag_unit.pins.append(SymbolPin(
|
||||
electricalType="power_out", graphicalStyle="line",
|
||||
position=Position(0, 0, 0), length=0, name="~", number="1",
|
||||
nameEffects=shown, numberEffects=shown))
|
||||
flag_lib.units.append(flag_unit)
|
||||
sch.libSymbols.append(flag_lib)
|
||||
for idx, net in enumerate(flag_nets, 1):
|
||||
fx = 1117.6 if idx <= 24 else 1168.4
|
||||
fy = 304.8 + ((idx - 1) % 24) * 12.7
|
||||
flag_uuid = duid(f"power-flag:{net}")
|
||||
flag = SchematicSymbol()
|
||||
flag.libId = "board:PWR_FLAG"
|
||||
flag.position = Position(fx, fy, 0)
|
||||
flag.unit = 1
|
||||
flag.inBom = False
|
||||
flag.onBoard = False
|
||||
flag.uuid = flag_uuid
|
||||
ref = f"#FLG{idx:03d}"
|
||||
flag.properties = [
|
||||
Property(key="Reference", value=ref, id=0,
|
||||
position=Position(fx, fy, 0), effects=hidden),
|
||||
Property(key="Value", value="PWR_FLAG", id=1,
|
||||
position=Position(fx, fy, 0), effects=hidden),
|
||||
Property(key="Footprint", value="", id=2,
|
||||
position=Position(fx, fy, 0), effects=hidden),
|
||||
Property(key="Datasheet", value="", id=3,
|
||||
position=Position(fx, fy, 0), effects=hidden),
|
||||
]
|
||||
flag.pins["1"] = duid(f"power-flag-pin:{net}")
|
||||
sch.schematicSymbols.append(flag)
|
||||
sch.globalLabels.append(GlobalLabel(
|
||||
text=net, shape="bidirectional", position=Position(fx, fy, 0), effects=shown,
|
||||
uuid=duid(f"power-flag-label:{net}")))
|
||||
sch.symbolInstances.append(SymbolInstance(
|
||||
path=f"/{flag_uuid}", reference=ref, unit=1, value="PWR_FLAG", footprint=""))
|
||||
sch.to_file(str(HW / "board.kicad_sch"))
|
||||
|
||||
external_symbols = copy.deepcopy(sch.libSymbols)
|
||||
for symbol in external_symbols:
|
||||
symbol.libraryNickname = None
|
||||
SymbolLib(version="20231120", generator="board_generator",
|
||||
symbols=external_symbols).to_file(str(HW / "board.kicad_sym"))
|
||||
(HW / "sym-lib-table").write_text(
|
||||
'(sym_lib_table\n (version 7)\n'
|
||||
' (lib (name "board")(type "KiCad")(uri "${KIPRJMOD}/board.kicad_sym")(options "")(descr "Generated board symbols"))\n)\n')
|
||||
(HW / "fp-lib-table").write_text(
|
||||
'(fp_lib_table\n (version 7)\n'
|
||||
' (lib (name "board")(type "KiCad")(uri "${KIPRJMOD}/board.pretty")(options "")(descr "Official-land-pattern custom footprints"))\n)\n')
|
||||
|
||||
|
||||
def main() -> None:
|
||||
(HW / "board-cache.lib").write_text(symbol_library())
|
||||
assignments = {sheet: [] for sheet in SHEETS}
|
||||
for comp in components:
|
||||
assignments[category(comp)].append(comp)
|
||||
for idx, sheet in enumerate(SHEETS, 1):
|
||||
(HW / f"{sheet}.sch").write_text(sheet_file(sheet, assignments[sheet], idx))
|
||||
(HW / "board.sch").write_text(root_file())
|
||||
write_boms()
|
||||
write_native_schematic()
|
||||
(HW / "board.kicad_pro").write_text(
|
||||
'{\n "meta": { "filename": "board.kicad_pro", "version": 1 },\n'
|
||||
' "erc": {}, "net_settings": {}, "text_variables": {}\n}\n')
|
||||
print(f"generated {len(components)} components on {len(SHEETS)} hierarchical sheets")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,599 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create the placed eight-layer KiCad PCB from the audited design database.
|
||||
|
||||
The script is deterministic so placement, net assignments, stackup, and rules
|
||||
can be regenerated and independently audited. Routing uses the staged
|
||||
BGA/plane/Freerouting flow in scripts/README.md; finalize_routing.py replaces
|
||||
autorouted RF/USB copper with audited deterministic geometry.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import re
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
import pcbnew
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
from design_data import components # noqa: E402
|
||||
|
||||
HW = ROOT / "hardware"
|
||||
BOARD_FILE = HW / "board.kicad_pcb"
|
||||
STOCK = Path("/usr/share/kicad/footprints")
|
||||
|
||||
# The distributed SMT test pads and removal of the former bottom test-point
|
||||
# field allow a compact 155 x 90 mm outline.
|
||||
X0, Y0, X1, Y1 = 20.0, 20.0, 175.0, 110.0
|
||||
|
||||
|
||||
def mm(value: float) -> int:
|
||||
return pcbnew.FromMM(value)
|
||||
|
||||
|
||||
def point(x: float, y: float) -> pcbnew.VECTOR2I:
|
||||
return pcbnew.VECTOR2I_MM(x, y)
|
||||
|
||||
|
||||
def footprint_path(identifier: str) -> tuple[Path, str]:
|
||||
library, name = identifier.split(":", 1)
|
||||
if library == "board":
|
||||
return HW / "board.pretty", name
|
||||
return STOCK / f"{library}.pretty", name
|
||||
|
||||
|
||||
MAJOR = {
|
||||
# FPGA/configuration. All four PMODs are on the north edge, close to the
|
||||
# BGA side carrying their I/O banks. Rotation makes the connector field
|
||||
# shallow and keeps all modules clear of the RF and IMU regions.
|
||||
"U1": (108, 72, 0), "U26": (97, 54, 0),
|
||||
"JPMOD1": (78, 24, 0), "JPMOD2": (96, 24, 0),
|
||||
"JPMOD3": (114, 24, 0), "JPMOD4": (132, 24, 0),
|
||||
|
||||
# USB-C and debug block.
|
||||
"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),
|
||||
"U29": (60, 54.5, 0), "U30": (67, 58.5, 0), "U31": (62, 63.5, 0),
|
||||
|
||||
# Mechanically quiet IMU region.
|
||||
"U22": (51, 84, 0),
|
||||
|
||||
# Protected input and switching-power block.
|
||||
"J3": (24, 100, 90), "U7": (34, 96, 0), "U8": (42, 104, 0), "U9": (46, 96, 0),
|
||||
"U10": (54, 104, 0), "U11": (59, 104, 0), "U12": (64, 94, 0),
|
||||
# High-current FPGA rails sit directly south of the BGA. This keeps the
|
||||
# 15-A core path and both 6-A I/O-rail paths short enough to enter their
|
||||
# local plane patches without long high-current traces.
|
||||
"U13": (100, 93, 0), "U14": (115, 97, 0), "U15": (130, 97, 0), "U16": (90, 98, 0),
|
||||
"U17": (143, 63, 0), "U18": (136, 67, 0), "U19": (152, 67, 0), "U20": (163, 63, 0),
|
||||
"U28": (141, 72.5, 0), "U32": (136, 74.5, 0), "U34": (88, 88, 0),
|
||||
"Q1": (70, 62, 0),
|
||||
|
||||
# User reset controls remain edge-accessible; only the tiny protection and
|
||||
# open-drain circuitry sit inward from the connector/button.
|
||||
"J4": (73, 106.5, 180), "SW1": (82, 106, 0), "D2": (78, 100.5, 0),
|
||||
"U33": (87, 103, 0),
|
||||
|
||||
# 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),
|
||||
"J5": (171, 56, 0), "U27": (165, 52, 0), "U21": (155.5, 45, 0),
|
||||
"U23": (149, 53, 0), "U24": (137, 55, 0),
|
||||
}
|
||||
|
||||
# Parts whose exact physical location materially affects signal integrity,
|
||||
# power-loop area, or mechanical performance.
|
||||
SPECIAL = {
|
||||
# RF series parts are collinear. L4 pad 2 lands directly on the SMA feed
|
||||
# rather than creating a bias-tee stub; the optional shunt-tuning pad 1s
|
||||
# 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),
|
||||
"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),
|
||||
"FB1": (60.0, 88.5, 0), "FB2": (36.0, 49.0, 0), "FB3": (39.0, 46.5, 0),
|
||||
"R63": (37.0, 55.50, 0), "R64": (37.0, 54.50, 0),
|
||||
# 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.
|
||||
"R45": (151.0, 44.5, 0),
|
||||
"R46": (154.6, 50.2, 90), "R47": (155.8, 50.2, 90),
|
||||
"R48": (157.0, 50.2, 90), "R49": (158.2, 50.2, 90),
|
||||
"R50": (160.5, 47.5, 90),
|
||||
"R51": (121.5, 65.5, 0), "R52": (121.5, 66.7, 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),
|
||||
# 40-MHz termination is at U24. SafeSPI controller outputs are terminated
|
||||
# beside the FPGA; MISO and EXTRESN bias remain beside the IMU.
|
||||
"R56": (140.2, 54.35, 0),
|
||||
"R57": (102.0, 58.8, 0), "R58": (104.0, 58.8, 0),
|
||||
"R59": (106.0, 58.8, 0), "R60": (60.0, 83.6, 0),
|
||||
"R61": (41.0, 83.6, 0),
|
||||
# Buffered FTDI interface-1 hard JTAG. Output-series parts sit directly
|
||||
# after the power-domain buffer; the TDO part is retained at the FPGA side.
|
||||
"R70": (65.0, 52.0, 0), "R71": (65.0, 53.2, 0),
|
||||
"R72": (65.0, 54.4, 0), "R73": (96.0, 70.0, 0),
|
||||
"R74": (96.0, 71.2, 0),
|
||||
}
|
||||
|
||||
# Small SMT bring-up pads live beside the circuit they measure; there is no
|
||||
# connector-like test-point bank along the board edge.
|
||||
SPECIAL.update({
|
||||
"TP1": (37.5, 94.0, 0), "TP2": (45.5, 104.0, 0),
|
||||
"TP3": (166.5, 54.0, 0), "TP4": (29.0, 39.0, 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),
|
||||
"TP9": (118.0, 102.0, 0), "TP10": (133.0, 102.0, 0),
|
||||
"TP11": (146.0, 61.0, 0), "TP12": (137.0, 70.0, 0),
|
||||
"TP13": (97.0, 58.0, 0), "TP14": (99.0, 58.0, 0),
|
||||
"TP15": (91.5, 103.0, 0),
|
||||
# Added configuration/JTAG pulls and reset-domain support.
|
||||
"R89": (96.0, 65.0, 0),
|
||||
"R90": (96.0, 66.2, 0), "R91": (96.0, 67.4, 0), "R92": (96.0, 68.6, 0),
|
||||
"R93": (96.0, 72.4, 0), "R94": (96.0, 73.6, 0),
|
||||
"R95": (96.0, 74.8, 0), "R96": (96.0, 76.0, 0),
|
||||
"R97": (59.0, 65.5, 0), "R98": (145.0, 74.5, 0),
|
||||
"R99": (73.0, 60.0, 0), "R100": (73.0, 61.2, 0), "R101": (73.0, 62.4, 0),
|
||||
"R102": (84.0, 100.5, 0), "R103": (80.0, 102.0, 0),
|
||||
"C120": (145.0, 72.5, 0), "C121": (82.0, 102.0, 0),
|
||||
"C122": (142.0, 76.5, 0), "C123": (64.0, 48.5, 0),
|
||||
"C124": (69.5, 56.0, 0), "C125": (62.0, 66.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),
|
||||
"R106": (83.0, 89.2, 0), "C128": (93.0, 88.0, 0),
|
||||
})
|
||||
|
||||
|
||||
# Critical placement references must never silently drift when parts are added
|
||||
# earlier in design_data.py. A mismatch here would corrupt RF geometry or put
|
||||
# a source-series resistor at the receiver end of a long digital trace.
|
||||
CRITICAL_REF_NETS = {
|
||||
"C83": {"RF_SMA", "RF_PRE_SAW"},
|
||||
"C84": {"RF_POST_SAW", "GND"},
|
||||
"R39": {"RF_POST_SAW", "MAX_MIXIN_HI"},
|
||||
"C85": {"MAX_MIXIN_HI", "GND"},
|
||||
"R56": {"OSC40_OUT", "CLK_40M_FPGA"},
|
||||
"R57": {"IMU_SCK", "IMU_SCK_DEV"},
|
||||
"R58": {"IMU_MOSI", "IMU_MOSI_DEV"},
|
||||
"R59": {"IMU_CS", "IMU_CS_DEV"},
|
||||
"R60": {"IMU_MISO", "IMU_MISO_DEV"},
|
||||
}
|
||||
_component_by_ref = {component.ref: component for component in components}
|
||||
for _ref, _expected_nets in CRITICAL_REF_NETS.items():
|
||||
_actual_nets = {pin.net for pin in _component_by_ref[_ref].pins if pin.net}
|
||||
assert _actual_nets == _expected_nets, (_ref, _actual_nets, _expected_nets)
|
||||
|
||||
|
||||
def local_offsets(count: int, clearance_radius: float) -> list[tuple[float, float]]:
|
||||
"""Return dense candidate rings for collision-aware group placement."""
|
||||
result: list[tuple[float, float]] = []
|
||||
ring = 0
|
||||
while len(result) < count:
|
||||
r = clearance_radius + ring * 1.20
|
||||
slots = max(12, int(2 * math.pi * r / 0.80))
|
||||
for idx in range(slots):
|
||||
angle = -math.pi / 2 + idx * 2 * math.pi / slots
|
||||
result.append((r * math.cos(angle), r * math.sin(angle)))
|
||||
if len(result) == count:
|
||||
return result
|
||||
ring += 1
|
||||
return result
|
||||
|
||||
|
||||
def courtyard_box(fp: pcbnew.FOOTPRINT, margin: float = 0.12) -> tuple[float, float, float, float]:
|
||||
layers = pcbnew.LSET()
|
||||
layers.AddLayer(pcbnew.F_CrtYd)
|
||||
layers.AddLayer(pcbnew.B_CrtYd)
|
||||
box = fp.GetLayerBoundingBox(layers)
|
||||
return (pcbnew.ToMM(box.GetX()) - margin, pcbnew.ToMM(box.GetY()) - margin,
|
||||
pcbnew.ToMM(box.GetRight()) + margin, pcbnew.ToMM(box.GetBottom()) + margin)
|
||||
|
||||
|
||||
def boxes_overlap(a: tuple[float, float, float, float], b: tuple[float, float, float, float]) -> bool:
|
||||
return a[0] < b[2] and a[2] > b[0] and a[1] < b[3] and a[3] > b[1]
|
||||
|
||||
|
||||
def add_text(board: pcbnew.BOARD, value: str, x: float, y: float, size: float = 1.0,
|
||||
layer: int = pcbnew.F_SilkS, angle: float = 0) -> None:
|
||||
text = pcbnew.PCB_TEXT(board)
|
||||
text.SetText(value)
|
||||
text.SetPosition(point(x, y))
|
||||
text.SetTextSize(pcbnew.VECTOR2I(mm(size), mm(size)))
|
||||
text.SetTextThickness(mm(max(0.15, size * 0.15)))
|
||||
text.SetLayer(layer)
|
||||
text.SetTextAngle(pcbnew.EDA_ANGLE(angle, pcbnew.DEGREES_T))
|
||||
board.Add(text)
|
||||
|
||||
|
||||
def add_outline(board: pcbnew.BOARD) -> None:
|
||||
for start, end in (((X0, Y0), (X1, Y0)), ((X1, Y0), (X1, Y1)),
|
||||
((X1, Y1), (X0, Y1)), ((X0, Y1), (X0, Y0))):
|
||||
seg = pcbnew.PCB_SHAPE(board)
|
||||
seg.SetShape(pcbnew.SHAPE_T_SEGMENT)
|
||||
seg.SetStart(point(*start))
|
||||
seg.SetEnd(point(*end))
|
||||
seg.SetWidth(mm(0.05))
|
||||
seg.SetLayer(pcbnew.Edge_Cuts)
|
||||
board.Add(seg)
|
||||
|
||||
|
||||
def configure_rules(board: pcbnew.BOARD) -> None:
|
||||
ds = board.GetDesignSettings()
|
||||
ds.SetBoardThickness(mm(1.6))
|
||||
ds.m_TrackMinWidth = mm(0.10)
|
||||
ds.m_MinClearance = mm(0.10)
|
||||
ds.m_ViasMinSize = mm(0.45)
|
||||
ds.m_MinThroughDrill = mm(0.225)
|
||||
ds.m_ViasMinAnnularWidth = mm(0.10)
|
||||
ds.m_CopperEdgeClearance = mm(0.25)
|
||||
ds.m_HoleClearance = mm(0.19)
|
||||
|
||||
settings = ds.m_NetSettings
|
||||
default = settings.GetDefaultNetclass()
|
||||
default.SetClearance(mm(0.10))
|
||||
default.SetTrackWidth(mm(0.12))
|
||||
default.SetViaDiameter(mm(0.45))
|
||||
default.SetViaDrill(mm(0.225))
|
||||
default.SetDiffPairWidth(mm(0.135))
|
||||
default.SetDiffPairGap(mm(0.15))
|
||||
|
||||
def netclass(name: str, clearance: float, width: float, via: float = 0.45, drill: float = 0.225,
|
||||
diff_width: float | None = None, diff_gap: float | None = None) -> None:
|
||||
nc = pcbnew.NETCLASS(name)
|
||||
nc.SetClearance(mm(clearance))
|
||||
nc.SetTrackWidth(mm(width))
|
||||
nc.SetViaDiameter(mm(via))
|
||||
nc.SetViaDrill(mm(drill))
|
||||
if diff_width is not None:
|
||||
nc.SetDiffPairWidth(mm(diff_width))
|
||||
if diff_gap is not None:
|
||||
nc.SetDiffPairGap(mm(diff_gap))
|
||||
settings.SetNetclass(name, nc)
|
||||
|
||||
netclass("BGA_ESCAPE", 0.10, 0.10)
|
||||
netclass("RF50_CPWG", 0.20, 0.164)
|
||||
netclass("USB90_DIFF", 0.15, 0.135, diff_width=0.135, diff_gap=0.15)
|
||||
netclass("CLOCK", 0.15, 0.15)
|
||||
# A 0.45/0.225-mm dogbone via with 0.10-mm clearance fits the Ti375's
|
||||
# 0.8-mm BGA pitch. The 0.40-mm trunk width is retained; Freerouting's
|
||||
# automatic neck-down is allowed only inside the escape field.
|
||||
netclass("POWER", 0.10, 0.40, 0.45, 0.225)
|
||||
|
||||
for pattern in ("RF_SMA", "RF_PRE_SAW", "RF_POST_SAW", "MAX_MIXIN_HI"):
|
||||
settings.SetNetclassPatternAssignment(pattern, "RF50_CPWG")
|
||||
for pattern in ("USB_D_P", "USB_D_N", "FTDI_D_P", "FTDI_D_N"):
|
||||
settings.SetNetclassPatternAssignment(pattern, "USB90_DIFF")
|
||||
# Use exact clock families so VCC3V0_CLK does not acquire an accidental
|
||||
# aggregate CLOCK,POWER effective class.
|
||||
for pattern in ("MAX_CLKOUT*", "MAX_SCLK*", "MAX_ADC_CLKIN", "QSPI_SCLK",
|
||||
"CLK_40M_FPGA", "OSC40_OUT", "TCXO16_OUT", "FTDI_OSC12",
|
||||
"FTDI_OSCI", "IMU_SCK*", "*TCK*"):
|
||||
settings.SetNetclassPatternAssignment(pattern, "CLOCK")
|
||||
for pattern in ("VCC*", "SYS5", "VBUS_RAW", "USB5_PROT", "EXT5_*", "GND", "CORE_SW",
|
||||
"BUCK1V8_SW", "BUCK3V3_SW", "ANT_BIAS_SEL", "ANT_BIAS_SW",
|
||||
"FTDI_1V8", "FTDI_VPHY", "FTDI_VPLL", "IMU_3V3",
|
||||
"IMU_VREGA2", "IMU_VREGD", "USB_SHIELD"):
|
||||
settings.SetNetclassPatternAssignment(pattern, "POWER")
|
||||
settings.SetNetclassPatternAssignment("EXP_*", "BGA_ESCAPE")
|
||||
settings.SetNetclassPatternAssignment("PMOD*", "BGA_ESCAPE")
|
||||
|
||||
|
||||
def add_stackup(board: pcbnew.BOARD) -> None:
|
||||
board.SetCopperLayerCount(8)
|
||||
board.SetLayerName(pcbnew.In1_Cu, "L2_GND")
|
||||
board.SetLayerName(pcbnew.In2_Cu, "L3_SIG")
|
||||
board.SetLayerName(pcbnew.In3_Cu, "L4_PWR")
|
||||
board.SetLayerName(pcbnew.In4_Cu, "L5_SIG")
|
||||
board.SetLayerName(pcbnew.In5_Cu, "L6_SIG")
|
||||
board.SetLayerName(pcbnew.In6_Cu, "L7_GND")
|
||||
board.SetLayerType(pcbnew.In1_Cu, pcbnew.LT_POWER)
|
||||
board.SetLayerType(pcbnew.In3_Cu, pcbnew.LT_POWER)
|
||||
board.SetLayerType(pcbnew.In6_Cu, pcbnew.LT_POWER)
|
||||
|
||||
|
||||
def place_components(board: pcbnew.BOARD) -> dict[str, pcbnew.FOOTPRINT]:
|
||||
net_items: dict[str, pcbnew.NETINFO_ITEM] = {}
|
||||
for comp in components:
|
||||
for pin in comp.pins:
|
||||
if pin.net and pin.net not in net_items:
|
||||
item = pcbnew.NETINFO_ITEM(board, pin.net)
|
||||
board.Add(item)
|
||||
net_items[pin.net] = item
|
||||
|
||||
loaded: dict[str, pcbnew.FOOTPRINT] = {}
|
||||
current_group = "U1"
|
||||
groups: dict[str, list] = defaultdict(list)
|
||||
for comp in components:
|
||||
if comp.ref in MAJOR:
|
||||
current_group = comp.ref
|
||||
if comp.ref not in MAJOR and comp.ref not in SPECIAL:
|
||||
if comp.description.startswith("Ti375 ") or "Efinix mandatory" in comp.description:
|
||||
groups["U1"].append(comp)
|
||||
else:
|
||||
groups[current_group].append(comp)
|
||||
|
||||
group_positions: dict[str, list[tuple[float, float]]] = {}
|
||||
for group, members in groups.items():
|
||||
radius = 4.0
|
||||
if group == "U1":
|
||||
radius = 11.5
|
||||
elif group in {"U2", "U13", "U21", "U22"}:
|
||||
radius = 5.5
|
||||
# Extra candidates let the actual courtyard collision pass skip places
|
||||
# occupied by adjacent functional blocks and larger bulk capacitors.
|
||||
group_positions[group] = local_offsets(max(1000, len(members) * 40), radius)
|
||||
|
||||
automatic: list[tuple[object, str]] = []
|
||||
current_group = "U1"
|
||||
for comp in components:
|
||||
path, name = footprint_path(comp.footprint)
|
||||
fp = pcbnew.FootprintLoad(str(path), name)
|
||||
if fp is None:
|
||||
raise RuntimeError(f"Cannot load {comp.footprint} for {comp.ref}")
|
||||
fp.SetReference(comp.ref)
|
||||
fp.SetValue(comp.value)
|
||||
fp.Reference().SetVisible(False)
|
||||
fp.Value().SetVisible(False)
|
||||
fp.Reference().SetLayer(pcbnew.F_Fab)
|
||||
fp.Value().SetLayer(pcbnew.F_Fab)
|
||||
# Dense components retain complete assembly/Fab graphics, while the
|
||||
# production silkscreen is limited to functional labels and the IMU
|
||||
# orientation mark so it cannot print on fine-pitch copper.
|
||||
for graphic in fp.GraphicalItems():
|
||||
if graphic.GetLayer() == pcbnew.F_SilkS:
|
||||
graphic.SetLayer(pcbnew.F_Fab)
|
||||
if comp.ref in MAJOR:
|
||||
current_group = comp.ref
|
||||
x, y, rotation = MAJOR[comp.ref]
|
||||
elif comp.ref in SPECIAL:
|
||||
x, y, rotation = SPECIAL[comp.ref]
|
||||
else:
|
||||
group = "U1" if (comp.description.startswith("Ti375 ") or "Efinix mandatory" in comp.description) else current_group
|
||||
ax, ay, _ = MAJOR[group]
|
||||
x, y, rotation = ax, ay, 0
|
||||
automatic.append((comp, group))
|
||||
fp.SetPosition(point(x, y))
|
||||
fp.SetOrientationDegrees(rotation)
|
||||
if comp.dnp:
|
||||
fp.SetDNP(True)
|
||||
fp.SetExcludedFromPosFiles(True)
|
||||
fp.SetField("MPN", comp.mpn)
|
||||
fp.SetField("Manufacturer", comp.manufacturer)
|
||||
for field_name in ("MPN", "Manufacturer"):
|
||||
field = fp.GetField(field_name)
|
||||
field.SetVisible(False)
|
||||
field.SetLayer(pcbnew.F_Fab)
|
||||
board.Add(fp)
|
||||
loaded[comp.ref] = fp
|
||||
|
||||
pad_map: dict[str, list[pcbnew.PAD]] = defaultdict(list)
|
||||
for physical_pad in fp.Pads():
|
||||
if physical_pad.GetNumber():
|
||||
pad_map[physical_pad.GetNumber()].append(physical_pad)
|
||||
for pin in comp.pins:
|
||||
for physical_pad in pad_map[str(pin.number)]:
|
||||
if pin.net:
|
||||
physical_pad.SetNet(net_items[pin.net])
|
||||
|
||||
# Place all automatic local parts after the fixed functional floorplan is
|
||||
# known, using their actual KiCad courtyards rather than nominal body size.
|
||||
occupied = [courtyard_box(loaded[ref]) for ref in (*MAJOR.keys(), *SPECIAL.keys()) if ref in loaded]
|
||||
search_start: dict[str, int] = defaultdict(int)
|
||||
for comp, group in automatic:
|
||||
fp = loaded[comp.ref]
|
||||
ax, ay, _ = MAJOR[group]
|
||||
candidates = group_positions[group]
|
||||
placed = False
|
||||
for idx in range(search_start[group], len(candidates)):
|
||||
dx, dy = candidates[idx]
|
||||
x, y = ax + dx, ay + dy
|
||||
fp.SetPosition(point(x, y))
|
||||
candidate = courtyard_box(fp)
|
||||
# Keep assembly courtyards on-board; edge connectors are all fixed
|
||||
# members and are intentionally permitted to protrude.
|
||||
if candidate[0] < X0 + 0.5 or candidate[2] > X1 - 0.5 or candidate[1] < Y0 + 0.5 or candidate[3] > Y1 - 0.5:
|
||||
continue
|
||||
if any(boxes_overlap(candidate, used) for used in occupied):
|
||||
continue
|
||||
occupied.append(candidate)
|
||||
search_start[group] = idx + 1
|
||||
placed = True
|
||||
break
|
||||
if not placed:
|
||||
raise RuntimeError(f"Could not place {comp.ref} near {group} without courtyard overlap")
|
||||
board.BuildListOfNets()
|
||||
return loaded
|
||||
|
||||
|
||||
def add_mounting_holes(board: pcbnew.BOARD) -> None:
|
||||
path = STOCK / "MountingHole.pretty"
|
||||
# The lower-left power connector owns the corner; offset H3 so its screw
|
||||
# head and courtyard cannot collide with J3.
|
||||
for idx, (x, y) in enumerate(((24, 24), (171, 24), (31, 106), (171, 106)), 1):
|
||||
fp = pcbnew.FootprintLoad(str(path), "MountingHole_3.2mm_M3")
|
||||
if fp is None:
|
||||
raise RuntimeError("Missing KiCad M3 mounting-hole footprint")
|
||||
fp.SetReference(f"H{idx}")
|
||||
fp.Reference().SetVisible(False)
|
||||
fp.SetPosition(point(x, y))
|
||||
board.Add(fp)
|
||||
|
||||
|
||||
def add_zone(board: pcbnew.BOARD, net_name: str, layer: int,
|
||||
bounds: tuple[float, float, float, float], priority: int = 0) -> None:
|
||||
"""Add a retained solid inner-layer plane region.
|
||||
|
||||
These regions are retained even before fanout vias exist so the Specctra
|
||||
export contains real plane targets for the router. The final refill then
|
||||
clears anti-pads around every unrelated via and plated hole.
|
||||
"""
|
||||
net = board.FindNet(net_name)
|
||||
if net is None:
|
||||
raise RuntimeError(f"Cannot create plane for missing net {net_name}")
|
||||
x0, y0, x1, y1 = bounds
|
||||
zone = pcbnew.ZONE(board)
|
||||
zone.SetLayer(layer)
|
||||
zone.SetNet(net)
|
||||
zone.SetLocalClearance(mm(0.20))
|
||||
zone.SetMinThickness(mm(0.10))
|
||||
zone.SetThermalReliefGap(mm(0.20))
|
||||
zone.SetThermalReliefSpokeWidth(mm(0.25))
|
||||
zone.SetPadConnection(pcbnew.ZONE_CONNECTION_FULL)
|
||||
zone.SetIslandRemovalMode(pcbnew.ISLAND_REMOVAL_MODE_NEVER)
|
||||
zone.SetAssignedPriority(priority)
|
||||
outline = zone.Outline()
|
||||
outline.NewOutline()
|
||||
for x, y in ((x0, y0), (x1, y0), (x1, y1), (x0, y1)):
|
||||
outline.Append(point(x, y))
|
||||
board.Add(zone)
|
||||
|
||||
|
||||
def add_power_planes(board: pcbnew.BOARD) -> None:
|
||||
# Continuous reference planes immediately below the two outer signal
|
||||
# layers. A 0.50-mm inset exceeds JLCPCB's copper-to-edge requirement.
|
||||
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))
|
||||
|
||||
# L4 is a deliberately split DC plane. Islands follow the functional
|
||||
# floorplan; no high-speed route uses L4 and no signal crosses a split on
|
||||
# either adjacent routing layer without a nearby return transition.
|
||||
islands = (
|
||||
("VCC3V3_AUX", (20.50, 38.00, 67.00, 72.00)),
|
||||
("VCC3V3", (39.00, 73.00, 68.00, 85.10)),
|
||||
("SYS5", (20.50, 85.50, 90.00, 109.50)),
|
||||
("SYS5", (90.40, 106.40, 140.00, 109.50)),
|
||||
# Core balls are interspersed with GND/auxiliary balls, so a complete
|
||||
# local patch (with automatic anti-pads) replaces the invalid left/right
|
||||
# voltage split that previously crossed the BGA field.
|
||||
("VCC0V95", (95.00, 60.50, 119.00, 94.00)),
|
||||
("VCC3V3", (119.50, 87.00, 140.00, 106.00)),
|
||||
("VCC3V0_CLK", (126.00, 50.00, 148.50, 72.00)),
|
||||
("VCC3V0_DIG", (119.50, 60.50, 125.50, 72.00)),
|
||||
("VCC3V0_DIG", (148.90, 50.40, 164.00, 72.00)),
|
||||
("VCC3V3_ANT", (164.40, 50.40, 174.50, 70.00)),
|
||||
("VCC3V0_RF", (145.00, 36.00, 174.50, 50.00)),
|
||||
)
|
||||
for priority, (net_name, bounds) in enumerate(islands, 1):
|
||||
add_zone(board, net_name, pcbnew.In3_Cu, bounds, priority)
|
||||
|
||||
# Local BGA supply patches. L2/L7 remain uninterrupted ground planes;
|
||||
# only the area below and immediately south of U1 is reserved on these
|
||||
# otherwise routable signal layers.
|
||||
add_zone(board, "VCC1V8", pcbnew.In4_Cu, (97.50, 60.50, 122.00, 101.00), 50)
|
||||
add_zone(board, "VCC3V3", pcbnew.In5_Cu, (97.50, 60.50, 140.00, 101.00), 50)
|
||||
|
||||
|
||||
def add_temporary_critical_keepouts(board: pcbnew.BOARD) -> None:
|
||||
"""Reserve the layers below RF and USB while Freerouting is running.
|
||||
|
||||
F.Cu deliberately remains available so the autorouted critical nets reserve
|
||||
their own top-layer corridors. The rule areas prevent unrelated signal
|
||||
tracks and through vias from crossing underneath. finalize_routing.py
|
||||
removes these temporary areas before installing the audited copper.
|
||||
"""
|
||||
layers = pcbnew.LSET()
|
||||
for layer in (pcbnew.In2_Cu, pcbnew.In4_Cu, pcbnew.In5_Cu, pcbnew.B_Cu):
|
||||
layers.AddLayer(layer)
|
||||
for name, bounds in (
|
||||
("TEMP_RF_NO_DIGITAL_BELOW", (152.0, 36.0, 174.5, 49.5)),
|
||||
("TEMP_USB_NO_CROSSING_BELOW", (22.0, 41.5, 44.5, 57.5))):
|
||||
x0, y0, x1, y1 = bounds
|
||||
area = pcbnew.ZONE(board)
|
||||
area.SetZoneName(name)
|
||||
area.SetIsRuleArea(True)
|
||||
area.SetLayerSet(layers)
|
||||
area.SetDoNotAllowTracks(True)
|
||||
area.SetDoNotAllowVias(True)
|
||||
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_documentation_graphics(board: pcbnew.BOARD) -> None:
|
||||
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, "USB-C 5V", 31, 35.5, 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, "BIAS J5: 3V3-OFF-5V", 163, 72, 0.8)
|
||||
add_text(board, "RF - NO DIGITAL ROUTING BELOW", 151, 69, 0.8)
|
||||
add_text(board, "IMU PIN1", 42, 78, 0.8)
|
||||
add_text(board, "X+ -> Y+ ^", 51, 84, 0.8)
|
||||
add_text(board, "USB JTAG / UART", 55, 38.5, 0.8)
|
||||
add_text(board, "EXT_RST", 73, 101.5, 0.8)
|
||||
add_text(board, "FPGA RESET", 82, 109.0, 0.8)
|
||||
add_text(board, "PMOD1 3V3", 78, 30, 0.8)
|
||||
add_text(board, "PMOD2 3V3", 96, 30, 0.8)
|
||||
add_text(board, "PMOD3 1V8", 114, 30, 0.8)
|
||||
add_text(board, "PMOD4 1V8", 132, 30, 0.8)
|
||||
|
||||
|
||||
STACKUP = '''\t\t(stackup
|
||||
\t\t\t(layer "F.SilkS" (type "Top Silk Screen"))
|
||||
\t\t\t(layer "F.Paste" (type "Top Solder Paste"))
|
||||
\t\t\t(layer "F.Mask" (type "Top Solder Mask") (thickness 0.01))
|
||||
\t\t\t(layer "F.Cu" (type "copper") (thickness 0.035))
|
||||
\t\t\t(layer "dielectric 1" (type "prepreg") (thickness 0.0994) (material "JLC 3313") (epsilon_r 4.1) (loss_tangent 0.02))
|
||||
\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))
|
||||
\t\t\t(layer "L3_SIG" (type "copper") (thickness 0.0152))
|
||||
\t\t\t(layer "dielectric 3" (type "prepreg") (thickness 0.3568) (material "JLC 3x2116") (epsilon_r 4.16) (loss_tangent 0.02))
|
||||
\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))
|
||||
\t\t\t(layer "L5_SIG" (type "copper") (thickness 0.0152))
|
||||
\t\t\t(layer "dielectric 5" (type "prepreg") (thickness 0.3568) (material "JLC 3x2116") (epsilon_r 4.16) (loss_tangent 0.02))
|
||||
\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))
|
||||
\t\t\t(layer "L7_GND" (type "copper") (thickness 0.0152))
|
||||
\t\t\t(layer "dielectric 7" (type "prepreg") (thickness 0.0994) (material "JLC 3313") (epsilon_r 4.1) (loss_tangent 0.02))
|
||||
\t\t\t(layer "B.Cu" (type "copper") (thickness 0.035))
|
||||
\t\t\t(layer "B.Mask" (type "Bottom Solder Mask") (thickness 0.01))
|
||||
\t\t\t(layer "B.Paste" (type "Bottom Solder Paste"))
|
||||
\t\t\t(layer "B.SilkS" (type "Bottom Silk Screen"))
|
||||
\t\t\t(copper_finish "ENIG")
|
||||
\t\t\t(dielectric_constraints yes)
|
||||
\t\t)'''
|
||||
|
||||
|
||||
def inject_stackup(path: Path) -> None:
|
||||
data = path.read_text()
|
||||
if "\n\t\t(stackup\n" in data:
|
||||
data = re.sub(r"\n\t\t\(stackup\n.*?\n\t\t\)\n", "\n" + STACKUP + "\n", data, count=1, flags=re.S)
|
||||
else:
|
||||
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)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
board = pcbnew.BOARD()
|
||||
add_stackup(board)
|
||||
configure_rules(board)
|
||||
add_outline(board)
|
||||
loaded = place_components(board)
|
||||
add_mounting_holes(board)
|
||||
add_power_planes(board)
|
||||
add_temporary_critical_keepouts(board)
|
||||
add_documentation_graphics(board)
|
||||
title = board.GetTitleBlock()
|
||||
title.SetTitle("Ti375 C529 GPS L1/L1C + SCH16T acquisition board")
|
||||
title.SetRevision("A0")
|
||||
title.SetCompany("JLCPCB manufacturing design")
|
||||
title.SetComment(0, "USB-C 5 V primary; protected EXT_5V; no back-feed")
|
||||
title.SetComment(1, "JLC08161H-3313 exact eight-layer construction")
|
||||
if not pcbnew.ZONE_FILLER(board).Fill(board.Zones()):
|
||||
raise RuntimeError("Initial inner-plane fill failed")
|
||||
pcbnew.SaveBoard(str(BOARD_FILE), board)
|
||||
inject_stackup(BOARD_FILE)
|
||||
print(f"Placed {len(loaded)} schematic components plus four NPTH mounting holes")
|
||||
print(f"Saved {BOARD_FILE}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compensate Freerouting 2.3.0's KiCad-10 DSN clearance scale mismatch.
|
||||
|
||||
KiCad exports widths and clearances in micrometres. Freerouting 2.3.0 keeps
|
||||
the widths but internally imports DSN clearance values at one quarter of the
|
||||
intended physical value. Scaling only ``(clearance N)`` entries by four makes
|
||||
the router's physical rules match the source KiCad board. Coordinates, track
|
||||
widths, vias, drills, padstacks, and keepouts are deliberately untouched.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
CLEARANCE = re.compile(r"(\(clearance\s+)(\d+(?:\.\d+)?)")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("input", type=Path)
|
||||
parser.add_argument("output", type=Path)
|
||||
parser.add_argument("--factor", type=float, default=4.0)
|
||||
args = parser.parse_args()
|
||||
|
||||
source = args.input.read_text()
|
||||
original = [float(match.group(2)) for match in CLEARANCE.finditer(source)]
|
||||
if not original:
|
||||
raise RuntimeError(f"No clearance rules found in {args.input}")
|
||||
|
||||
def scale(match: re.Match[str]) -> str:
|
||||
value = float(match.group(2)) * args.factor
|
||||
rendered = str(int(value)) if value.is_integer() else f"{value:g}"
|
||||
return match.group(1) + rendered
|
||||
|
||||
converted, count = CLEARANCE.subn(scale, source)
|
||||
scaled = [float(match.group(2)) for match in CLEARANCE.finditer(converted)]
|
||||
assert count == len(original) == len(scaled)
|
||||
assert all(after == before * args.factor
|
||||
for before, after in zip(original, scaled, strict=True))
|
||||
# A conversion must never touch the board geometry or trace-width rules.
|
||||
assert source.count("(width ") == converted.count("(width ")
|
||||
assert source.count("(polygon ") == converted.count("(polygon ")
|
||||
args.output.write_text(converted)
|
||||
print(f"Scaled {count} clearance rules by {args.factor:g}x")
|
||||
print(f"Saved {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Install deterministic 0.8-mm-pitch dogbone escapes for connected Ti375 balls."""
|
||||
|
||||
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.kicad_pcb"
|
||||
DEFAULT_OUTPUT = ROOT / "hardware" / "board_prerouted.kicad_pcb"
|
||||
POWER_NETS = {
|
||||
"GND", "VCC0V95", "VCC1V8", "VCC3V3", "VCC3V0_DIG",
|
||||
}
|
||||
|
||||
|
||||
def mm(value: float) -> int:
|
||||
return pcbnew.FromMM(value)
|
||||
|
||||
|
||||
def add_segment(board: pcbnew.BOARD, pad: pcbnew.PAD,
|
||||
end: pcbnew.VECTOR2I, width_mm: float) -> None:
|
||||
track = pcbnew.PCB_TRACK(board)
|
||||
track.SetStart(pad.GetPosition())
|
||||
track.SetEnd(end)
|
||||
track.SetWidth(mm(width_mm))
|
||||
track.SetLayer(pcbnew.F_Cu)
|
||||
track.SetNet(pad.GetNet())
|
||||
track.SetLocked(True)
|
||||
board.Add(track)
|
||||
|
||||
|
||||
def add_via(board: pcbnew.BOARD, pad: pcbnew.PAD,
|
||||
position: pcbnew.VECTOR2I) -> None:
|
||||
via = pcbnew.PCB_VIA(board)
|
||||
via.SetPosition(position)
|
||||
via.SetWidth(mm(0.45))
|
||||
via.SetDrill(mm(0.225))
|
||||
via.SetLayerPair(pcbnew.F_Cu, pcbnew.B_Cu)
|
||||
via.SetNet(pad.GetNet())
|
||||
via.SetLocked(True)
|
||||
board.Add(via)
|
||||
|
||||
|
||||
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))
|
||||
fpga = board.FindFootprintByReference("U1")
|
||||
if fpga is None:
|
||||
raise RuntimeError("U1 Ti375 footprint is missing")
|
||||
centre = fpga.GetPosition()
|
||||
occupied: set[tuple[int, int]] = set()
|
||||
counts = {"power": 0, "signal": 0}
|
||||
|
||||
for pad in fpga.Pads():
|
||||
if not pad.GetNetname():
|
||||
continue
|
||||
position = pad.GetPosition()
|
||||
# Move each ball toward its package quadrant. Centre-row/column balls
|
||||
# use the positive direction, leaving a full 0.8-mm via grid without
|
||||
# duplicated interstitial locations.
|
||||
dx = 0.4 if position.x >= centre.x else -0.4
|
||||
dy = 0.4 if position.y >= centre.y else -0.4
|
||||
via_position = pcbnew.VECTOR2I(position.x + mm(dx), position.y + mm(dy))
|
||||
key = (via_position.x, via_position.y)
|
||||
if key in occupied:
|
||||
raise RuntimeError(f"Duplicate dogbone via at {key} for U1.{pad.GetNumber()}")
|
||||
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_via(board, pad, via_position)
|
||||
counts["power" if is_power else "signal"] += 1
|
||||
|
||||
if not pcbnew.ZONE_FILLER(board).Fill(board.Zones()):
|
||||
raise RuntimeError("Zone fill failed after BGA fanout")
|
||||
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"Installed {counts['power']} power/GND and {counts['signal']} signal dogbones")
|
||||
print(f"Saved {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,195 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Connect SMT power/ground pads locally to their intended solid planes.
|
||||
|
||||
This deliberately does not replace power-stage routing. It only installs
|
||||
short pad-to-via connections where a filled, same-net plane is physically
|
||||
present below the candidate via. High-current converter loops and plane
|
||||
bridges are handled separately.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import math
|
||||
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_plane_stitched.kicad_pcb"
|
||||
PLANE_NETS = {
|
||||
"GND", "SYS5", "VCC0V95", "VCC1V8", "VCC3V3", "VCC3V3_AUX",
|
||||
"VCC3V0_CLK", "VCC3V0_DIG", "VCC3V0_RF", "VCC3V3_ANT",
|
||||
}
|
||||
VIA_DIAMETER = 0.45
|
||||
VIA_DRILL = 0.225
|
||||
CLEARANCE = 0.10
|
||||
|
||||
|
||||
def mm(value: float) -> int:
|
||||
return pcbnew.FromMM(value)
|
||||
|
||||
|
||||
def distance_mm(a: pcbnew.VECTOR2I, b: pcbnew.VECTOR2I) -> float:
|
||||
return math.hypot(pcbnew.ToMM(a.x - b.x), pcbnew.ToMM(a.y - b.y))
|
||||
|
||||
|
||||
def add_track(board: pcbnew.BOARD, pad: pcbnew.PAD,
|
||||
end: pcbnew.VECTOR2I, width: float) -> None:
|
||||
track = pcbnew.PCB_TRACK(board)
|
||||
track.SetStart(pad.GetPosition())
|
||||
track.SetEnd(end)
|
||||
track.SetWidth(mm(width))
|
||||
track.SetLayer(pad.GetLayer())
|
||||
track.SetNet(pad.GetNet())
|
||||
track.SetLocked(True)
|
||||
board.Add(track)
|
||||
|
||||
|
||||
def add_via(board: pcbnew.BOARD, pad: pcbnew.PAD,
|
||||
position: pcbnew.VECTOR2I) -> pcbnew.PCB_VIA:
|
||||
via = pcbnew.PCB_VIA(board)
|
||||
via.SetPosition(position)
|
||||
via.SetWidth(mm(VIA_DIAMETER))
|
||||
via.SetDrill(mm(VIA_DRILL))
|
||||
via.SetLayerPair(pcbnew.F_Cu, pcbnew.B_Cu)
|
||||
via.SetNet(pad.GetNet())
|
||||
via.SetLocked(True)
|
||||
board.Add(via)
|
||||
return via
|
||||
|
||||
|
||||
def plane_at(zones_by_net: dict[str, list[pcbnew.ZONE]], net: str,
|
||||
position: pcbnew.VECTOR2I) -> bool:
|
||||
return any(zone.HitTestFilledArea(zone.GetLayer(), position)
|
||||
for zone in zones_by_net.get(net, ()))
|
||||
|
||||
|
||||
def clear_of_other_copper(position: pcbnew.VECTOR2I, net: str,
|
||||
pads: list[pcbnew.PAD], vias: list[pcbnew.PCB_VIA]) -> bool:
|
||||
x, y = pcbnew.ToMM(position.x), pcbnew.ToMM(position.y)
|
||||
margin = VIA_DIAMETER / 2 + CLEARANCE + 0.03
|
||||
for other in pads:
|
||||
if other.GetNetname() == net:
|
||||
continue
|
||||
box = other.GetBoundingBox()
|
||||
x0, y0 = pcbnew.ToMM(box.GetX()) - margin, pcbnew.ToMM(box.GetY()) - margin
|
||||
x1, y1 = pcbnew.ToMM(box.GetRight()) + margin, pcbnew.ToMM(box.GetBottom()) + margin
|
||||
if x0 <= x <= x1 and y0 <= y <= y1:
|
||||
return False
|
||||
for other in vias:
|
||||
minimum = (VIA_DIAMETER + pcbnew.ToMM(other.GetWidth(pcbnew.F_Cu))) / 2 + CLEARANCE
|
||||
if distance_mm(position, other.GetPosition()) < minimum:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def clear_track_path(start: pcbnew.VECTOR2I, end: pcbnew.VECTOR2I, net: str,
|
||||
source: pcbnew.PAD, pads: list[pcbnew.PAD], width: float) -> bool:
|
||||
"""Conservatively reject short dogbones that cross another pad bbox."""
|
||||
margin = width / 2 + CLEARANCE + 0.02
|
||||
for step in range(1, 12):
|
||||
fraction = step / 12
|
||||
sample = pcbnew.VECTOR2I(
|
||||
round(start.x + (end.x - start.x) * fraction),
|
||||
round(start.y + (end.y - start.y) * fraction),
|
||||
)
|
||||
x, y = pcbnew.ToMM(sample.x), pcbnew.ToMM(sample.y)
|
||||
for other in pads:
|
||||
if other is source or other.GetNetname() == net:
|
||||
continue
|
||||
box = other.GetBoundingBox()
|
||||
if (pcbnew.ToMM(box.GetX()) - margin <= x <= pcbnew.ToMM(box.GetRight()) + margin and
|
||||
pcbnew.ToMM(box.GetY()) - margin <= y <= pcbnew.ToMM(box.GetBottom()) + margin):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def candidates(pad: pcbnew.PAD, footprint: pcbnew.FOOTPRINT) -> list[pcbnew.VECTOR2I]:
|
||||
pad_pos = pad.GetPosition()
|
||||
fp_pos = footprint.GetPosition()
|
||||
away = math.atan2(pad_pos.y - fp_pos.y, pad_pos.x - fp_pos.x)
|
||||
if pad_pos == fp_pos:
|
||||
away = 0.0
|
||||
radius = max(pcbnew.ToMM(pad.GetSizeX()), pcbnew.ToMM(pad.GetSizeY())) / 2
|
||||
radius += VIA_DIAMETER / 2 + CLEARANCE + 0.08
|
||||
angles = [away]
|
||||
for delta_deg in (30, -30, 60, -60, 90, -90, 120, -120, 150, -150, 180):
|
||||
angles.append(away + math.radians(delta_deg))
|
||||
result = []
|
||||
for scale in (1.0, 1.35, 1.75):
|
||||
for angle in angles:
|
||||
result.append(pcbnew.VECTOR2I(
|
||||
pad_pos.x + mm(radius * scale * math.cos(angle)),
|
||||
pad_pos.y + mm(radius * scale * math.sin(angle)),
|
||||
))
|
||||
return result
|
||||
|
||||
|
||||
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))
|
||||
if not pcbnew.ZONE_FILLER(board).Fill(board.Zones()):
|
||||
raise RuntimeError("Could not fill planes before stitching")
|
||||
|
||||
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()):
|
||||
raise RuntimeError("Could not refill planes after stitching")
|
||||
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("Plane connections:", ", ".join(f"{n}={k}" for n, k in sorted(connected.items())))
|
||||
print("Pads without a local same-net plane:",
|
||||
", ".join(f"{n}={k}" for n, k in sorted(skipped.items())))
|
||||
print(f"Saved {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user