Initial Ti375 GNSS IMU board design and documentation

This commit is contained in:
2026-08-15 18:09:17 +03:00
commit b706197667
69 changed files with 132859 additions and 0 deletions
+144
View File
@@ -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 |
+794
View File
@@ -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()
+52
View File
@@ -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()
+129
View File
@@ -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()
+246
View File
@@ -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()
+258
View File
@@ -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()
+300
View File
@@ -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()
+460
View File
@@ -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()
+599
View File
@@ -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()
+52
View File
@@ -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()
+94
View File
@@ -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()
+195
View File
@@ -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()