Compare commits
17 Commits
818f965896
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4d99f4210 | ||
|
|
21648f6d6c | ||
|
|
ac8108c3f2 | ||
|
|
8124b411bf | ||
|
|
dbfe4b22f7 | ||
|
|
f7eb69929e | ||
|
|
39146a7d4c | ||
|
|
2f239bb75d | ||
|
|
fb19caa95d | ||
|
|
13496b302c | ||
|
|
261bf48bb4 | ||
|
|
d72f3a4bd8 | ||
|
|
fd627f512b | ||
|
|
c469238dd5 | ||
|
|
18ef98a88e | ||
|
|
1d169476fb | ||
|
|
6082613e88 |
144
.claude/skills/kicad-build/SKILL.md
Normal file
144
.claude/skills/kicad-build/SKILL.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# kicad-build — PCB generation from opencode
|
||||
|
||||
Use this skill when building PCBs with the kicad-claude-toolkit inside opencode.
|
||||
It wires KiCad's pcbnew (only available in KiCad's bundled Python) into opencode's
|
||||
bash toolchain so you can generate .kicad_sch, .kicad_pcb, run ERC/DRC, and export
|
||||
gerbers — all from opencode.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- KiCad.app installed at `/Applications/KiCad/KiCad.app`
|
||||
- `kicad-cli` on PATH (`brew install kicad`)
|
||||
- `ngspice` on PATH for simulation (`brew install ngspice`)
|
||||
- `netlistsvg` on PATH for schematic SVG (`npm install -g netlistsvg`)
|
||||
- Run `tools/install-kicad-toolkit` once to install circuit_toolkit into KiCad's Python
|
||||
|
||||
## Key paths
|
||||
|
||||
| What | Path |
|
||||
|------|------|
|
||||
| KiCad Python (has pcbnew) | `/Applications/KiCad/KiCad.app/Contents/Frameworks/Python.framework/Versions/3.9/bin/python3` |
|
||||
| Launcher script | `tools/kicad-python` |
|
||||
| Build orchestrator | `tools/kicad-build` |
|
||||
| Install script | `tools/install-kicad-toolkit` |
|
||||
| circuit_toolkit source | `kicad-claude-toolkit/python/circuit_toolkit` |
|
||||
| S-expression editor | `tools/retire_block.py` |
|
||||
| KiCad CLI | `/opt/homebrew/bin/kicad-cli` (or `kicad-cli` on PATH) |
|
||||
|
||||
## How it works
|
||||
|
||||
The toolkit has two layers:
|
||||
|
||||
1. **circuit_toolkit** (Python) — describes circuit topology (components, nets) and
|
||||
generates `.kicad_sch` / `.kicad_pcb` files. The PCB builder (`builders/pcb.py`)
|
||||
calls `pcbnew` directly, so it MUST run inside KiCad's Python.
|
||||
2. **kicad-cli** (binary) — runs ERC, DRC, exports gerbers/BOM/PDF/netlist. Runs
|
||||
anywhere on PATH.
|
||||
|
||||
### The launcher
|
||||
|
||||
`tools/kicad-python` finds KiCad's Python and execs it. Use it for any script that
|
||||
imports `pcbnew` or `circuit_toolkit.builders.pcb`:
|
||||
|
||||
```bash
|
||||
./tools/kicad-python -c "from circuit_toolkit.builders.pcb import build_pcb"
|
||||
./tools/kicad-python my_script.py
|
||||
./tools/kicad-python -m circuit_toolkit.build board_dir/
|
||||
```
|
||||
|
||||
### The build orchestrator
|
||||
|
||||
`tools/kicad-build <board-dir> [--all]` runs the full pipeline:
|
||||
|
||||
```
|
||||
1. circuit_toolkit build → .kicad_sch + .kicad_pcb
|
||||
2. kicad-cli sch erc → ERC report
|
||||
3. kicad-cli pcb drc → DRC report
|
||||
4. kicad-cli pcb export → gerber + drill + position
|
||||
5. kicad-cli sch export → BOM + PDF + netlist
|
||||
6. ngspice -b → SPICE simulations (if sim/ present)
|
||||
```
|
||||
|
||||
Output lands in `<board-dir>/output/`.
|
||||
|
||||
## Writing a board
|
||||
|
||||
A board is a Python script using `circuit_toolkit.blocks`:
|
||||
|
||||
```python
|
||||
# my-board/build.py
|
||||
from circuit_toolkit import Board
|
||||
from circuit_toolkit.blocks import usbc_power, ams1117_ldo, led_indicator, pin_header, m2_mounting_hole
|
||||
|
||||
board = Board("my-board", size=(48, 30))
|
||||
vbus, gnd, cc1, cc2 = usbc_power(board, ref="J1", cc_pulldowns="5.1k")
|
||||
v3v3 = ams1117_ldo(board, ref="U1", vin=vbus, gnd=gnd, output_voltage=3.3)
|
||||
led_indicator(board, ref_led="D1", ref_resistor="R3", vin=v3v3, gnd=gnd, color="red")
|
||||
pin_header(board, ref="J2", pins=2, label="3V3_OUT", nets=[v3v3, gnd])
|
||||
for ref in ("H1", "H2", "H3", "H4"):
|
||||
m2_mounting_hole(board, ref=ref)
|
||||
```
|
||||
|
||||
Then run:
|
||||
```bash
|
||||
./tools/kicad-python build.py
|
||||
```
|
||||
|
||||
A separate `layout.py` provides component positions + tracks + vias + zones, passed
|
||||
to `build_pcb()`.
|
||||
|
||||
## Editing KiCad s-expressions directly
|
||||
|
||||
For surgical edits to `.kicad_sch` or `.kicad_pcb` s-expressions (removing blocks,
|
||||
flooded net cleanup, no-connect markers), use `tools/retire_block.py`:
|
||||
|
||||
```bash
|
||||
python3 tools/retire_block.py carrier/CM5IO.kicad_sch \
|
||||
--symbols J7,U18 --nets 'SD_*' \
|
||||
--apply
|
||||
```
|
||||
|
||||
## Common opencode patterns
|
||||
|
||||
### Generate a PCB from a board definition
|
||||
```bash
|
||||
cd <board-dir>
|
||||
./tools/kicad-python build.py
|
||||
```
|
||||
|
||||
### Run ERC after schematic changes
|
||||
```bash
|
||||
kicad-cli sch erc *.kicad_sch --output output/erc/erc_report.txt
|
||||
```
|
||||
|
||||
### Run DRC after layout changes
|
||||
```bash
|
||||
kicad-cli pcb drc *.kicad_pcb --output output/drc/drc_report.html
|
||||
```
|
||||
|
||||
### Export gerbers for fab
|
||||
```bash
|
||||
kicad-cli pcb export gerbers *.kicad_pcb --output output/fab/gerber
|
||||
kicad-cli pcb export drill *.kicad_pcb --output output/fab/gerber
|
||||
kicad-cli pcb export pos *.kicad_pcb --output output/fab/positions.csv
|
||||
```
|
||||
|
||||
### Render 3D PCB view
|
||||
```bash
|
||||
kicad-cli pcb render *.kicad_pcb -o output/3d.png
|
||||
```
|
||||
|
||||
### SPICE simulation
|
||||
```bash
|
||||
ngspice -b -r output/sim/run.raw sim/circuit.cir
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **"pcbnew not found"**: use `tools/kicad-python` instead of system python3
|
||||
- **"kicad-cli not found"**: `brew install kicad`
|
||||
- **"ngspice not found"**: `brew install ngspice`
|
||||
- **KiCad updates**: re-run `tools/install-kicad-toolkit` after KiCad updates
|
||||
- **Python 3.9 vs 3.10**: KiCad ships 3.9; toolkit pyproject says >=3.10. The
|
||||
`--no-build-isolation` flag in install-kicad-toolkit bypasses this (code is
|
||||
compatible with 3.9).
|
||||
93
.claude/skills/kicad-port/SKILL.md
Normal file
93
.claude/skills/kicad-port/SKILL.md
Normal file
@@ -0,0 +1,93 @@
|
||||
---
|
||||
name: kicad-port
|
||||
description: "Project skill for porting the Raspberry Pi CM5IO reference design into this custom CM5 carrier board. Use this skill on EVERY schematic/PCB task in this repo: editing .kicad_sch, defining or changing pinouts and nets, adding/removing circuit blocks, or verifying a design change. It encodes the source-of-truth for CM5 pins, the s-expression editing rules, and the MANDATORY verify loop (kicad-cli ERC + kicad-happy analyzer + diff-vs-reference). Always consult this skill before editing any KiCad file here, and run the verify loop after."
|
||||
---
|
||||
|
||||
# CM5 Carrier — Port Workflow
|
||||
|
||||
We are building a custom Raspberry Pi **CM5 carrier board** by porting the official
|
||||
**CM5IO reference design**. Work is currently in the **schematic / pinout phase**.
|
||||
|
||||
This is a **diff-from-reference** project: every change starts from a known-good reference
|
||||
and must stay electrically defensible against it. Claude is the *editor* (surgical s-expr
|
||||
text edits); the tooling below is the *verifier*. Never edit without verifying.
|
||||
|
||||
## Source of truth (consult before any pin/net decision)
|
||||
|
||||
1. `cm5-datasheet.pdf` — authoritative CM5 module pinout, power sequencing, pin reservations.
|
||||
2. `CM5_Carrier_Pinout_BOM.md` / `CM5_Carrier_Design.md` — this board's intended pin map and BOM.
|
||||
3. `refs/CM5IO.kicad_sch` (+ `CM5_GPIO`, `CM5_HighSpeed`, `PCIe-M2` sub-sheets) — the reference
|
||||
schematic we port FROM. Hierarchical design; sub-sheets are referenced by UUID.
|
||||
|
||||
If a pin assignment disagrees between the datasheet and any other doc, **the datasheet wins** —
|
||||
flag the discrepancy, don't silently pick one.
|
||||
|
||||
## S-expression editing rules (footguns)
|
||||
|
||||
KiCad `.kicad_sch` / `.kicad_pcb` files are s-expression text — readable and Edit-able.
|
||||
But:
|
||||
|
||||
- **NEVER hand-edit or hand-invent UUIDs.** Copy-pasting a symbol/sheet block as text and
|
||||
reusing its UUID corrupts the schematic (KiCad treats duplicates as the same object).
|
||||
If you need a new instance, generate a fresh UUID:
|
||||
`python3 -c "import uuid; print(uuid.uuid4())"`
|
||||
- **Hierarchical nets:** a net can carry multiple labels across sheets (the analyzer's `LB-001`
|
||||
finding). Prefer one canonical label style per cross-sheet net; don't rename one half.
|
||||
- **Power nets need a driver:** every power rail needs a power symbol or `PWR_FLAG`, or ERC fails.
|
||||
- After ANY structural edit, re-run the verify loop below. A passing Edit is not a passing design.
|
||||
- Coordinate edits through git. Another (context-free) agent may touch these files; commit small,
|
||||
review diffs, never blind-overwrite a sheet you didn't just read.
|
||||
|
||||
## The verify loop (MANDATORY after every schematic change)
|
||||
|
||||
`python3` here is Homebrew 3.14 (the kicad-happy scripts require Python ≥ 3.10).
|
||||
Scripts live under `~/.claude/skills/kicad/scripts/` (symlinked from `kicad-happy/`).
|
||||
|
||||
```bash
|
||||
SCH=path/to/your.kicad_sch # the file you changed
|
||||
KSCR=~/.claude/skills/kicad/scripts
|
||||
|
||||
# 1. KiCad's own ERC (the hard gate — must be clean before commit)
|
||||
kicad-cli sch erc --exit-code-violations --output /tmp/erc.json --format json "$SCH"
|
||||
# non-zero exit => fix or revert before proceeding.
|
||||
|
||||
# 2. kicad-happy structural review (catches what ERC won't: decoupling gaps,
|
||||
# connector ground ratios, protocol/voltage mismatches, multi-label nets)
|
||||
python3 $KSCR/analyze_schematic.py "$SCH" --text # human-readable
|
||||
python3 $KSCR/analyze_schematic.py "$SCH" -o /tmp/head.json # machine-readable
|
||||
|
||||
# 3. Diff against the committed reference baseline to see what your change moved
|
||||
python3 $KSCR/diff_analysis.py analysis/baseline/cm5io.json /tmp/head.json --text
|
||||
```
|
||||
|
||||
Treat new WARN/ERROR findings in step 2/3 as regressions to justify or fix — not noise.
|
||||
|
||||
## Optional deeper checks
|
||||
|
||||
- **SPICE** (`spice` skill): validates analog subcircuits (regulator feedback dividers, RC/LC
|
||||
filters, crystal load caps). Requires a simulator on PATH — **`ngspice` IS installed**
|
||||
(`/opt/homebrew/bin/ngspice`) and the loop is validated end-to-end on the reference
|
||||
(8 subcircuits pass, incl. the 5V→3.3V buck feedback divider). Run after the analyzer:
|
||||
`python3 ~/.claude/skills/spice/scripts/simulate_subcircuits.py <analysis.json> -o sim.json`.
|
||||
- **Datasheets** (`datasheets` skill): extract CM5 / IC specs from PDFs so analyzer findings are
|
||||
`datasheet-backed` rather than `heuristic`. Start with `cm5-datasheet.pdf`.
|
||||
|
||||
## Schematic hygiene checklist (before declaring a sheet done)
|
||||
|
||||
- [ ] All nets named — no auto-generated `Net-(R1-Pad1)` names in the final design
|
||||
- [ ] Power rails ALL_CAPS (`+3V3`, `+5V`, `GND`); active-low uses `n` prefix (`nRESET`, `nCS`)
|
||||
- [ ] `PWR_FLAG` on every power net with no explicit driver
|
||||
- [ ] No-connect markers on all intentionally unconnected pins
|
||||
- [ ] Hierarchical port names match the nets they carry
|
||||
- [ ] Reference designators follow convention (U/R/C/L/D/Q/J/SW/F/TP/BT), annotated by block
|
||||
- [ ] Title block filled (project, rev, date, author, one-line description)
|
||||
- [ ] Design notes added for non-obvious choices (pull-up values, protection ratings, placement)
|
||||
- [ ] **ERC passes with zero errors** (step 1 above)
|
||||
|
||||
## Reference shelf (not wired into the loop)
|
||||
|
||||
- `MCP-KiCad/` — MCP server over `pcbnew` (SWIG). PCB/fabrication only, Linux/flatpak-first.
|
||||
Ignore until layout phase, and even then prefer `kicad-cli pcb` over it.
|
||||
- `kicad-claude-toolkit/` — greenfield "circuit-as-Python → PCB" generator + IPC bridge.
|
||||
Built for new designs, not porting. Only its `schematic-hygiene` guidance (folded in above)
|
||||
is relevant now.
|
||||
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# --- KiCad transient / generated ---
|
||||
*.lck
|
||||
~*.lck
|
||||
*.kicad_prl
|
||||
*-backups/
|
||||
fp-info-cache
|
||||
*.bak
|
||||
*.kicad_sch-bak
|
||||
_autosave-*
|
||||
|
||||
# --- macOS ---
|
||||
.DS_Store
|
||||
|
||||
# --- editor / MCP local artifacts ---
|
||||
.history/
|
||||
.playwright-mcp/
|
||||
|
||||
# --- External tooling clones (kept locally, not vendored into this repo) ---
|
||||
# These are git clones with their own history; the kicad-port skill references
|
||||
# kicad-happy via ~/.claude/skills symlinks. Re-clone if missing.
|
||||
/kicad-happy/
|
||||
/kicad-claude-toolkit/
|
||||
/MCP-KiCad/
|
||||
|
||||
# --- Analysis cache (timestamped runs); committed baselines live in analysis/baseline/ ---
|
||||
.kicad-happy/
|
||||
analysis/runs/
|
||||
|
||||
# --- kicad-build toolchain output ---
|
||||
output/
|
||||
*.raw
|
||||
*.xml
|
||||
|
||||
# --- python cache ---
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -94,7 +94,7 @@ A+E key edge. Populate PCIe + control; leave USB pins NC.
|
||||
| CLKREQ0# | PCIE_CLKREQ# | CM5 PCIe CLKREQ |
|
||||
| REFCLK+ / REFCLK− | PCIE_REFCLK_P/N | CM5 PCIe refclk (100 MHz) |
|
||||
| PETp0/PETn0 | PCIE_TX_P/N | CM5 PCIe TX (AC-coupled on CM5) |
|
||||
| PERp0/PERn0 | PCIE_RX_P/N | CM5 PCIe RX (AC-couple caps on carrier) |
|
||||
| PERp0/PERn0 | PCIE_RX_P/N | CM5 PCIe RX (**direct** — card supplies its own TX coupling per M.2; no carrier caps) |
|
||||
| W_DISABLE1# (RF_KILL) | WIFI_DISABLE# | pull-up + optional GPIO |
|
||||
| 3.3 V pins | +3V3_RF | from 15→3.3 V 4 A RF buck (direct) |
|
||||
| GND | GND | |
|
||||
@@ -221,7 +221,7 @@ PoE pins of the magjack: leave **unloaded / NC** (no PoE in this design).
|
||||
| 6 | J_ETH | 1 | RJ45 1:1 GbE magjack | Pulse JXD0-0001NL / Bel V890-1AX | 2–4 |
|
||||
| 7 | ESD_ETH | 1 | Ethernet ESD array | TI TPD4E1U06 / Bourns | 0.3–0.6 |
|
||||
| 8 | J_E | 1 | M.2 E-key (A+E) 2230 conn + standoff | Amphenol/Attend 119A-92A00 | 1.2–2 |
|
||||
| 9 | C_PCIE | 4 | PCIe RX AC-couple 0.1 µF | GRM 0402 100 nF | 0.05 |
|
||||
| ~~9~~ | ~~C_PCIE~~ | 0 | **REMOVED** — PCIe RX coupling lives on the M.2 card's TX (standard M.2); CM5 couples its own TX. No carrier caps. | — |
|
||||
| 10 | J_USB1/2 | 2 | USB-A USB3.0 right-angle | Amphenol UE27AC54100 | 0.8–1.4 ea |
|
||||
| 11 | J_UPROG | 1 | USB-C 16-pin (USB2 + CC) | GCT USB4085 | 0.5–1 |
|
||||
| 12 | ESD_USB | 3 | USB ESD array | ST USBLC6-2SC6 | 0.15 ea |
|
||||
@@ -267,7 +267,7 @@ DF40 pair (line 1) = ~45–60% of BOM. Everything else commodity.
|
||||
|
||||
1. Import CM5 KiCad symbol from `refs/CM5IO.kicad_sym` — pin map is now resolved in `CM5_Carrier_PinMap.md` (all 200 pins).
|
||||
2. **SYNC (pin 18) is 3.3 V** — drive directly from the Schmitt via a series R; **no level translator** (corrected).
|
||||
3. AC-couple PCIe **RX** on carrier (TX coupled on CM5).
|
||||
3. PCIe **RX is NOT coupled on the carrier** — caps live on the M.2 card's TX (standard M.2); CM5 couples its own TX. (Corrected: earlier text/BOM said carrier-side; double-coupling would break the link.)
|
||||
4. CC 5.1 kΩ pulldowns on USB-C; nRPIBOOT jumper logic.
|
||||
5. Magjack PoE pins NC; Bob-Smith + ESD on cable side.
|
||||
6. Both 15→3.3 buck inductors/thermals sized for their rails (RF 4 A, AUX 1 A); all bucks rated ≥20 V Vin, low-duty-capable.
|
||||
|
||||
192
PORT_STATUS.md
Normal file
192
PORT_STATUS.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# CM5 Carrier Port — Status & Handoff
|
||||
|
||||
> Handoff doc for whoever (human or LLM) continues this work. Read this first, then
|
||||
> `.claude/skills/kicad-port/SKILL.md` (the mandatory workflow/verify-loop), then
|
||||
> `CM5_Carrier_Design.md` (frozen rev-A spec).
|
||||
|
||||
## ▶ RESUME HERE (session handoff, 2026-07-04 — close-out passes COMPLETE, ready for layout prep)
|
||||
|
||||
**State:** branch `port/strip-phase` (merged → `main`), working tree clean. Sheets in
|
||||
`carrier/`: root `CM5IO`, `CM5_GPIO`, `CM5_HighSpeed` (reference, stripped), `M2_Ekey`,
|
||||
`Power` (stages 1–4), `GPS_PPS`. All rev-A blocks captured; **power-budget review +
|
||||
datasheet confirms + hygiene pass done** (see "Power-budget review" section below).
|
||||
Baseline anchor: `analysis/baseline/carrier_stripped.json`. Whole-design **ERC = 133**
|
||||
(all pre-existing types; a good edit adds **zero new**); kicad-cli annotation warning GONE;
|
||||
BOM sane (105 components, all with value+footprint); SPICE 24 pass / 2 benign warn / 0 fail.
|
||||
|
||||
**Close-out fixes landed (2026-07-04):**
|
||||
- Removed orphaned `U12` (RT9742) + `C13` — fed the stripped `HDMI_5v` rail (ref-netlist-proven).
|
||||
- **Cut J11 (USB-C prog) VBUS from `+5v`** + dropped `R9` bleed — host VBUS would have
|
||||
paralleled U_BUCK5's output (reference is USB-C-powered; carrier is not). VBUS pins NC;
|
||||
CC1/CC2 stay wired to CM5 (module implements the PD sink per SC1466 — supersedes the
|
||||
spec's discrete 5.1 k Rd); D± unchanged → rpiboot unaffected (CM5 has no VBUS-sense pin).
|
||||
- `C_BULK1` 100 µF **25 V→35 V** (sits on VIN_PROT where SMBJ18A clamps ≤29.2 V).
|
||||
- Refdes hygiene: all generated refs digit-terminated (`J_E`→`J_E1`, `C_VCAP`→`C_VCAP1`, …,
|
||||
`#FLG15IN`→`#FLG15IN1`); generator emit-helpers now normalize tags. Title blocks: carrier
|
||||
identity, rev A, CM5IO derivation credited.
|
||||
|
||||
**NEXT ACTION → layout prep:** copy DF40 (CM5 connector) placement from the reference PCB,
|
||||
then floorplan: Power stage entry edge, RF keepout around M.2, PPS trace hygiene.
|
||||
Open items (decide at layout/BOM time, none block layout start — details in the review section):
|
||||
per-port USB-A ILIM vs shared 1.19 A (spec deviation), ESD array on J11 D± (spec BOM item 12,
|
||||
reference shipped without), M.2 socket contact rating ≥0.9 A/pin, J8 header draw guidance
|
||||
(≤1 A on 5 V, ≤400 mA on 3.3 V), L33A1 Isat 2.5 A vs TPS54202 fault current (hiccup-protected).
|
||||
|
||||
**How to build/verify (every change):**
|
||||
- Sheets are generated wholesale — edit `tools/build_ekey.py` / `tools/build_power.py`, then
|
||||
`python3 tools/build_power.py --apply`. Custom IC symbols go in `carrier/CM5IO.kicad_sym`
|
||||
(bare name) AND embed in the sheet as `CM5IO:<name>` — else ERC `lib_symbol_issues`.
|
||||
- Verify loop (judge by the **integrated root** run, not standalone sub-sheets):
|
||||
`kicad-cli sch erc --format json -o /tmp/e.json carrier/CM5IO.kicad_sch` (track delta vs 133,
|
||||
zero new types) → `analyze_schematic.py carrier/CM5IO.kicad_sch` + `diff_analysis.py
|
||||
analysis/baseline/carrier_stripped.json …` (zero regressions) → for bucks, `simulate_subcircuits.py`.
|
||||
- Gotchas (full list in memory `port-progress`): grid-snap all coords to 1.27 mm or labels don't
|
||||
bind; PWR_FLAG/GND must sit on a real pin coord; pre-existing `+5v` is hierarchical (not global);
|
||||
type feedback pins `passive`; connector/module pin types may need remodeling for cross-part ERC.
|
||||
|
||||
## Power-budget review (2026-07-04, all datasheet-backed)
|
||||
|
||||
Tree: 15 V (J_PWR1, Phoenix MKDS-1.5 ≈13.5 A) → LM74700+CSD18540 ideal diode → `VIN_PROT`
|
||||
→ 3 bucks. Worst-case input ≈ 40 W / 15 V ≈ **2.9 A** — huge connector/FET margin.
|
||||
|
||||
| Rail | Source | Rating | Worst-case load | Verdict |
|
||||
|------|--------|--------|-----------------|---------|
|
||||
| `VIN_PROT` | LM74700 + CSD18540 (60 V, ~2 mΩ) | — | 2.9 A (≈20 mW in FET) | ✅ |
|
||||
| `+5v` | U_BUCK51 LM61460 (6 A; HS ILIM 8.9–11.5 A) | 6 A | CM5 (0.4 idle/0.9 typ/~3 A heavy, incl. its 3V3+1V8 outputs) + USB-A ≤1.53 A (U6 ILIM) + fan ~0.3 A + J8 5 V user | ✅ if J8 draw documented ≤1 A |
|
||||
| `+3V3_RF` | U_BUCK33R1 LM61460 (6 A) | 4 A design | AW7915 3–3.5 A TX bursts | ✅ ~2.5 A headroom |
|
||||
| `+3V3_AUX` | U_BUCK33A1 TPS54202 (2 A) | ~1 A design | GPS ~0.1 A + 74LVC1G17 ~mA | ✅ |
|
||||
| `CM5_3.3V` (module out) | CM5 | **600 mA total** | LEDs+magjack+U5 ≈50 mA + J8 3.3 V user | ✅ if J8 draw documented ≤400 mA |
|
||||
| `CM5_1.8V` (module out) | CM5 | 600 mA | R4 = nf → unloaded | ✅ |
|
||||
|
||||
**Datasheet confirms (closes all "confirm vs datasheet" leftovers):**
|
||||
- LM74700: C_VCAP min 0.1 µF, rec ≥10×Ciss(FET) → our 1 µF ✓; EN→ANODE = documented always-on ✓.
|
||||
- LM61460: RT 33.2 k→400 kHz table row; our 31.6 k ≈ 420 kHz, in 200 k–2.2 M range ✓.
|
||||
Ipk @5 A ≈ 5.8 A < HS ILIM min 8.9 A ✓.
|
||||
- L5/L33 (Bourns SRP1245A-4R7M): **Isat 15 A** > ILIM max 11.5 A ✓✓; Irms ~11 A ✓.
|
||||
- AP22653 (U6, R12=15 k): ILIM 0.94–1.53 A (typ 1.19 A, best-fit eqn ILIMIT_typ=30321/R^1.055),
|
||||
**shared across both USB-A ports** — spec §5 wanted 0.9–1.2 A *per port*. Deviation kept
|
||||
(reference-proven; protects the 5 V budget). Rev-B option: one AP22653 per port.
|
||||
- TPS54202: VIN abs-max 30 V vs SMBJ18A clamp ≤29.2 V — 0.8 V margin, accepted + documented.
|
||||
L33A1 (10 µH/2.5 A): Ipk @1 A ≈ 1.26 A ✓; fault current before hiccup can brush ~3 A >
|
||||
Isat — acceptable (hiccup-protected), consider Isat ≥3 A part at BOM time.
|
||||
|
||||
**Sequencing:** module drives its own 3.3/1.8 rails after `PMIC_EN`; `+3V3_RF` EN gated by
|
||||
CM5 `PCIE_PWR_EN` (up only after boot) ✓; `+3V3_AUX` EN = UVLO divider (start 10.1 V /
|
||||
stop 8.6 V) so GPS runs whenever 15 V is healthy ✓.
|
||||
|
||||
## What this repo is
|
||||
|
||||
Two intertwined goals:
|
||||
1. **Improve Claude's EDA tooling** — the [`kicad-happy`](https://github.com/aklofas/kicad-happy)
|
||||
skill suite (`kicad`, `datasheets`, `spice`, …), wired in as Claude Code skills via
|
||||
`~/.claude/skills/{kicad,datasheets,spice} -> kicad-happy/skills/*` symlinks. The toolkit
|
||||
clones (`kicad-happy/`, `kicad-claude-toolkit/`, `MCP-KiCad/`) are gitignored — re-clone if missing.
|
||||
2. **Port the Raspberry Pi CM5IO reference into a custom CM5 carrier board** — the real
|
||||
test bed (PTP/timing instrumentation node: 15V→5V front end, GbE magjack, M.2 E-key Wi-Fi
|
||||
(AW7915/MT7915), 2×USB-A, USB-C programming, RTC, GPS-PPS distribution).
|
||||
|
||||
Method is **diff-from-reference**: `refs/` holds the untouched CM5IO reference; `carrier/` is the
|
||||
working copy. Every edit is verified against `analysis/baseline/cm5io.json`.
|
||||
|
||||
## Environment / tooling state (macOS, Homebrew)
|
||||
|
||||
- `kicad-cli` ✅ (`/opt/homebrew/bin/kicad-cli`) — ERC + netlist export
|
||||
- `poppler` ✅ (pdftotext) — datasheet page selection
|
||||
- `ngspice` ✅ — `spice` skill; full verify loop validated on the reference (8 subcircuits pass)
|
||||
- Python ≥3.10 (Homebrew 3.14)
|
||||
|
||||
## The verify loop (run after EVERY schematic edit)
|
||||
|
||||
```bash
|
||||
SCH=carrier/CM5IO.kicad_sch ; KSCR=~/.claude/skills/kicad/scripts
|
||||
kicad-cli sch erc --exit-code-violations --format json -o /tmp/erc.json "$SCH" # hard gate
|
||||
python3 $KSCR/analyze_schematic.py "$SCH" -o /tmp/head.json # structural
|
||||
python3 $KSCR/diff_analysis.py analysis/baseline/cm5io.json /tmp/head.json --text # vs baseline
|
||||
```
|
||||
- **Track the ERC DELTA, not the absolute** — the reference already has 182 pre-existing
|
||||
violations (mostly `unconnected_wire_endpoint`). A good edit introduces **zero new types**.
|
||||
- The **analyzer diff is the semantic gate** (names broken design intent); **ERC is the hygiene gate**.
|
||||
- For kept-block integrity, also diff `kicad-cli sch export netlist` before/after — it caught a
|
||||
mis-inventoried part (see R8 below).
|
||||
|
||||
## Datasheet extraction (done)
|
||||
|
||||
CM5 module extracted to `datasheets/extracted/SC1466_43a9ec.json` (200 pins, score 8.7/10,
|
||||
queryable via `~/.claude/skills/datasheets/scripts/datasheet_features.py`). Drove the NC pin
|
||||
lists for every strip (e.g. HDMI Module1 pins = all pins whose datasheet name starts `HDMI`).
|
||||
|
||||
## The strip tool: `tools/retire_block.py`
|
||||
|
||||
General block-retirement for these s-expr sheets. Capabilities:
|
||||
- excise symbols by Reference; retire nets by name-glob (all label types: local / hierarchical / global)
|
||||
- **anchor-aware wire removal** (union-find): removes a wire only if its connected component
|
||||
reaches no *kept* anchor (kept non-power pin or kept label) AND is seeded by this edit — so
|
||||
shared nets survive and pre-existing dangles are untouched
|
||||
- place `(no_connect)` on exposed Module1 pins (pin coords via a validated transform
|
||||
`abs=(px+rot(lx,ly).x, py-rot(lx,ly).y)`, rot0/no-mirror; refuses other orientations)
|
||||
- clean orphaned power flags, dangling NCs, orphaned labels
|
||||
- `--sheet-ports NAME,...` removes root-sheet hierarchical pins + connecting wires
|
||||
|
||||
Usage: `python3 tools/retire_block.py <sheet> --symbols J7,U18 --nets 'SD_*' --nc-pins 57,61 [--sheet-ports ...] [--apply]` (dry-run without `--apply`).
|
||||
|
||||
**Known gap (next improvement):** does NOT auto-prune orphaned power-label/wire *stubs* left after a
|
||||
connector is removed (e.g. leftover `+3.3v`/`HDMI_5v` labels). Those were cleaned manually — see git history.
|
||||
|
||||
## Strip phase — DONE (microSD + HDMI + MIPI + PCIe-M2), ERC 182 → 133, 0 new violations, 0 regressions
|
||||
|
||||
| Block | Sheet(s) | Command(s) (run with `--apply`) |
|
||||
|---|---|---|
|
||||
| **microSD** | CM5_GPIO | `--symbols J7,U18,C5 --nets 'SD_*' --nc-pins 57,61,62,63,67,69,75` |
|
||||
| **HDMI** | CM5_HighSpeed | `--symbols J22,J10 --nets 'HDMI0_*,HDMI1_*,HDMI_5v' --nc-pins 143,145,146,147,148,149,151,152,153,154,158,160,164,166,170,172,176,178,182,184,188,190,199,200` |
|
||||
| **MIPI/camera** | CM5_HighSpeed, CM5_GPIO, CM5IO(root) | (1) HighSpeed `--symbols J5,J16 --nets 'DPHY0_*,DPHY1_*,SCL1,SDA1' --nc-pins 115,117,121,123,127,129,133,135,139,141,175,177,181,183,187,189,193,194,195,196` (2) GPIO `--nc-pins 80,82,97,100` (3) root `--sheet-ports SCL0,SDA0,CAM_GPIO0,CAM_GPIO1` (4) HighSpeed `--symbols R8 --nets 'SCL0,SDA0'` (5) manual: remove 3 orphaned +3.3v labels + 2 wire stubs |
|
||||
| **PCIe-M2 (M-key/NVMe)** | CM5_HighSpeed, CM5IO(root), `.kicad_pro` | (1) HighSpeed `--nets 'PCIE_*' --nc-pins 102,104,106,109,110,112,116,118,122,124` (2) root `--drop-sheet PCIe-M2.kicad_sch` (3) root `--sheet-ports PCIE_CLK_P,PCIE_CLK_N,PCIE_TX_P,PCIE_TX_N,PCIE_nRST,PCIE_RX_P,PCIE_RX_N,PCIE_nCLKREQ,PCIE_PWR_EN,PCIE_nWAKE` (strip dangling HighSpeed sheet-symbol pins) (4) `git rm carrier/PCIe-M2.kicad_sch` (5) manual: remove PCIe-M2 entry from `.kicad_pro` `sheets` array + its ERC exclusion. **Design confirmed: 1× M.2 E-key (Wi-Fi) only, 0× M-key SSD** — the single CM5 PCIe ×1 lane is reserved for the E-key; the freed `PCIE_*` lanes (now NC on Module1 unit 2) will be rewired to the fresh E-key sheet in the add phase. |
|
||||
|
||||
**`retire_block.py` gained `--drop-sheet FILE`**: removes the whole `(sheet …)` block by Sheetfile + its `sheet_instances` path (run on the parent sheet). `.kicad_pro` `sheets`-array + ERC-exclusion cleanup is still manual (next tool gap).
|
||||
|
||||
Notes that bit us (don't re-learn the hard way):
|
||||
- **MIPI lanes are labeled `DPHY0_*`/`DPHY1_*`, NOT `MIPI*`.** The connectors J5/J16 are camera FFCs
|
||||
bundling DPHY + cross-sheet I2C0 (`SCL0/SDA0`) + `CAM_GPIO0/1` — a 3-sheet hierarchical block.
|
||||
- **`R8` is a camera pull-up, not USB** (the Explore-agent inventory was wrong). Confirmed via
|
||||
`kicad-cli sch export netlist` (`R8.2 → J16.17`). Always verify inventory against the netlist.
|
||||
- **`SCL0/SDA0` (I2C0) are camera-only 2-node nets** (netlist-proven). NC'd to **reserve I2C0 for a
|
||||
future external I2C RTC** (CM5's own RTC is on-module via VBAT). If you add an external RTC, wire it here.
|
||||
|
||||
## Next steps
|
||||
|
||||
1. ~~Drop the PCIe-M2 sheet entirely~~ **DONE** (see strip table). Confirmed E-key-only (no M-key SSD).
|
||||
2. ~~Re-baseline~~ **DONE** — `analysis/baseline/carrier_stripped.json` is the add-phase diff anchor.
|
||||
3. **M.2 E-key sheet** ✅ **DONE** (`carrier/M2_Ekey.kicad_sch`, generated by `tools/build_ekey.py`).
|
||||
J_E = KiCad `Connector:Bus_M.2_Socket_E`, fp `Connector_PCBEdge:M.2_2230-xx-E`. PCIe link-0 wired to the
|
||||
9 freed CM5 pins via **global labels** (PCIE_TX/RX/CLK ±, nRST, nCLKREQ, nWAKE — netlist-confirmed J_E↔Module1);
|
||||
`+3V3_RF` (4 pins) + 10µF∥2×0.1µF decoupling + temp PWR_FLAG; 11 GND; W_DISABLE1/2 10k pull-ups to +3V3_RF;
|
||||
~41 NC (USB/BT, PCM/I2S, SDIO, UART, COEX, I2C, link-1). RX **not** coupled on carrier (card-side per M.2; BOM C_PCIE removed).
|
||||
**Verify:** ERC 133→133 (zero new); analyzer diff +7 added, **0 regressions**.
|
||||
Connector-symbol pin types remodeled for cross-connector ERC: lines CM5 drives → `passive`; lines the card
|
||||
drives into CM5 inputs (RX pair + CLKREQ#/WAKE#) → `output`.
|
||||
**Open items (review/next):** (a) CM5 `PCIE_PWR_EN` (pin 106) still NC — wire to the +3V3_RF buck EN in the
|
||||
power sheet; (b) the temp PWR_FLAG on +3V3_RF must be REMOVED once the buck drives it (else dual-driver ERC);
|
||||
(c) optionally route `WIFI_DISABLE#` to a CM5 GPIO for SW radio control; (d) analyzer CG-AUD 5:1 sig/gnd on J_E
|
||||
is inherent to the M.2 connector (INFO, not fixable). New tool: `tools/build_ekey.py` (place connector + auto-NC + labels).
|
||||
4. **Power sheet** (`carrier/Power.kicad_sch`, generated by `tools/build_power.py`) — staged, **sync + datasheet-built parts** (user choice). **Spec rating fix:** the spec's buck candidates miss its own ≥20V-Vin rule (TPS54424=17V, TPS54560=async) — selecting synchronous ≥30V parts instead (LM61460-class 6A for +5V & +3V3_RF/4A; TPS54202 28V for +3V3_AUX), each verified vs datasheet before commit.
|
||||
- **Stage 1 — input protection ✅ DONE.** J_PWR(15V) → D_REV `LM74700-Q1` ideal-diode + Q_REV N-FET (reverse-polarity/-current) → `VIN_PROT`; `SMBJ18A` TVS + 100µF/25V ∥ 10µF/50V bulk; C_VCAP charge-pump cap. Netlist-verified topology; ERC 133→133 (zero new); analyzer 0 regressions. **Confirm vs LM74700-Q1 datasheet:** C_VCAP value, EN-to-+15V_IN, FET pick (Vds≥30V, SOA/inrush) + optional gate R.
|
||||
- **Stage 2 — U_BUCK5 15→`+5v` 5A ✅ DONE.** `LM61460-Q1` (6A, 42V absmax sync; **custom symbol built from the datasheet** → `carrier/CM5IO.kicad_sym` as `CM5IO:LM61460-Q1`, fp `Package_SO:Texas_HTSSOP-14-1EP…ThermalVias`). VREF=1.0V → FB divider R_FBT5 40.2k / R_FBB5 10.0k = **5.02V (SPICE-confirmed vfb=1.0V)**. L5 4.7µH, Cout 2×47µF/16V, Cin 2×10µF/50V, Cboot 100n+Rboot 4.7Ω, RT 31.6k(~400kHz), BIAS→+5v, EN→VIN_PROT, PGOOD 100k→+5v. **`+5v` kept HIERARCHICAL** (pre-existing carrier net; global would collide `same_local_global_label`) — Power sheet exposes a `+5v` sheet pin, tied into the root `+5v` net; netlist-verified it drives Module1 + GPIO + USB. ERC 133→133 (zero new); analyzer +32 added **0 regressions**; SPICE 12/13 pass, 1 benign warn (L5/Cboot mis-detected as LC filter). **Confirm vs datasheet:** RT freq, L sat ≥7A, EP=pad15, optional EN UVLO divider.
|
||||
- **Stage 3 — U_BUCK33R 15→`+3V3_RF` 4A ✅ DONE.** Second `LM61460-Q1` (shared BOM with stage 2: L 4.7µH/7A ripple ~1.3A=33%, Cin 2×10µ/50V, Cout 2×47µ/16V, RT 31.6k ~420kHz, Cboot 100n+Rboot 4.7). FB divider R_FBT33 23.2k / R_FBB33 10.0k = **3.32V (SPICE: vfb 0.994V, inrush settles 3.32V)**. BIAS→`+3V3_RF` (datasheet: valid ≥3.1V, auto-falls back to VIN below). **EN ← CM5 `PCIE_PWR_EN`** (Module1 pin 106: NC swapped for a global label on `CM5_HighSpeed`) + **R_EN33 100k pull-down** (mirrors reference R14 — netlist-verified `Module1.106 + U_BUCK33R.7 + R_EN33.1`) so the Wi-Fi rail is OFF until the CM5 asserts it. **E-key temp `#FLG33RF` PWR_FLAG removed** (build_ekey); the buck's PWR_FLAG on L33.2 is now the sole `+3V3_RF` driver — netlist shows buck + all 4 J_E power pins + pull-ups + decoupling on one net. Verify: ERC 133→133 zero new; analyzer +49 added **0 regressions**; SPICE 17 pass / 2 benign warn (the L/C_BOOT "LC filter" mis-detect, once per buck) / 0 fail. (Note: `kicad-cli` netlist export prints a pre-existing "annotation errors" warning — un-numbered generated refdeses like `J_E`/`J_PWR`; present at fd627f5 too, cosmetic.)
|
||||
- **Stage 4 — U_BUCK33A 15→`+3V3_AUX` ~1A ✅ DONE (power sheet complete).** `TPS54202` (SOT-23-6, sync, fixed 500kHz, VFB=0.596V). **Abs-max gate PASSED but thin:** VIN abs-max **30V** vs SMBJ18A clamp ≤29.2V at full rated 600W surge → 0.8V transient margin (operating max 28V is not the surge number). Custom symbol `CM5IO:TPS54202` (the KiCad lib symbol `extends TPS54302` — no own pins, can't embed standalone) + bare copy in `carrier/CM5IO.kicad_sym`. FB per TI's 3.3V table row: **100k/22.1k + Cff 56pF → 3.29V (SPICE: vfb 0.597V, inrush settles 3.315V)**. L 10µH/2.5A (ΔI~0.52A), Cout 2×22µF/16V, Cboot 100n, Cin 10µF/50V+100n. **EN abs-max is only 7V → NO direct VIN tie** (unlike LM61460): UVLO divider **866k/110k** (solved from Ip=0.7µA/Ih=1.55µA, VEN 1.21/1.19V) → **start 10.13V / stop 8.61V**, EN ~1.9V at 15V in (~3.5V at 29V clamp — safe). FB+EN pins typed `passive` (divider-only nets). Verify: ERC 133→133 zero new; analyzer +65/−0 zero regressions (warn/error set identical to stage 3); SPICE 23 pass / 2 benign warn / 0 fail / 0 skip. **New tooling gap found:** `simulate_subcircuits.py` lowercases refdeses into SPICE names — `C_IN33A` vs stage-3 `C_IN33a` collided ("device already exists") and silently SKIPPED the whole VIN_PROT decoupling group → renamed to `C_IN33Aa`. Rule: **never create refdeses differing only by case.**
|
||||
5. **GPS + PPS distribution ✅ DONE** (`carrier/GPS_PPS.kicad_sch`, generated by `tools/build_gps.py`).
|
||||
Per Pinout_BOM §6 (the authority — it CORRECTS the design doc): J_GPS 5-pin (1=PPS, 2=NMEA_RX←GPS TX,
|
||||
3=NMEA_TX→GPS RX, 4=+3V3_AUX, 5=GND); `GPS_PPS_RAW` → TVS_PPS `PESD3V3L1BA` → U_PPS `74LVC1G17`
|
||||
(SOT-23-5, +3V3_AUX, pin 1 true-NC) → `PPS_CLEAN` → R_PPS 33Ω → `SYNC_OUT` + J_PPS1 (UWB) / J_PPS2
|
||||
(scope), 2-pin taps. **NMEA = UART2 on GPIO4/GPIO5** (correction C2; UART0/GPIO14-15 stays debug).
|
||||
Cross-sheet wiring: converted the 6 local labels (`SYNC_OUT`×2, `GPIO4`×2, `GPIO5`×2 on CM5_GPIO) to
|
||||
**global** labels — net names unchanged so no multi-label (LB-001) findings, ERC-neutral (133→133);
|
||||
GPS sheet uses the same global names, root sheet-symbol has NO pins. J_GPS pins 1/2 re-typed `output`
|
||||
(GPS module drives PPS + TX) so the analyzer's no_driver gate passes — same trick as the E-key socket.
|
||||
**Verify:** ERC 133→133 zero new; analyzer +73/−0 **0 regressions** (only the pre-existing MPN-coverage
|
||||
counter moved 50→57 parts); netlist confirms all 6 cross-sheet nets (SYNC_OUT = Module1.18+J2.6+R_PPS.2);
|
||||
SPICE 24 pass / 2 benign warn / 0 fail / 0 skip.
|
||||
|
||||
## Out-of-repo notes
|
||||
|
||||
Richer running notes live in this machine's Claude memory:
|
||||
`~/.claude/projects/-Users-noise-Documents-obsidian-rpiboard/memory/` (`project-cm5-eda`,
|
||||
`port-progress`, `eda-tooling-gaps`, `tooling-installs-cm5`). This doc is the portable subset.
|
||||
70876
analysis/baseline/carrier_stripped.json
Normal file
70876
analysis/baseline/carrier_stripped.json
Normal file
File diff suppressed because it is too large
Load Diff
220
analysis/baseline/cm5io_sim.json
Normal file
220
analysis/baseline/cm5io_sim.json
Normal file
@@ -0,0 +1,220 @@
|
||||
{
|
||||
"analyzer_type": "spice",
|
||||
"schema_version": "1.3.0",
|
||||
"summary": {
|
||||
"total": 8,
|
||||
"total_findings": 0,
|
||||
"pass": 8,
|
||||
"warn": 0,
|
||||
"fail": 0,
|
||||
"skip": 0,
|
||||
"by_severity": {
|
||||
"error": 0,
|
||||
"warning": 0,
|
||||
"info": 8
|
||||
}
|
||||
},
|
||||
"findings": [],
|
||||
"simulation_results": [
|
||||
{
|
||||
"subcircuit_type": "voltage_divider",
|
||||
"components": [
|
||||
"R15",
|
||||
"R16"
|
||||
],
|
||||
"expected": {
|
||||
"ratio": 0.180328,
|
||||
"vout_V": 0.5950823999999999
|
||||
},
|
||||
"simulated": {
|
||||
"vout_V": 0.595082
|
||||
},
|
||||
"delta": {
|
||||
"vout_error_pct": 0.0
|
||||
},
|
||||
"status": "pass",
|
||||
"elapsed_s": 1.214,
|
||||
"reference": "R15/R16"
|
||||
},
|
||||
{
|
||||
"subcircuit_type": "voltage_divider",
|
||||
"components": [
|
||||
"R15",
|
||||
"R16"
|
||||
],
|
||||
"expected": {
|
||||
"ratio": 0.180328,
|
||||
"vout_V": 0.5950823999999999
|
||||
},
|
||||
"simulated": {
|
||||
"vout_V": 0.595082
|
||||
},
|
||||
"delta": {
|
||||
"vout_error_pct": 0.0
|
||||
},
|
||||
"status": "pass",
|
||||
"elapsed_s": 0.021,
|
||||
"reference": "R15/R16"
|
||||
},
|
||||
{
|
||||
"subcircuit_type": "decoupling",
|
||||
"components": [
|
||||
"C18",
|
||||
"C9",
|
||||
"C12",
|
||||
"C6",
|
||||
"C4"
|
||||
],
|
||||
"rail": "/00000000-0000-0000-0000-00005ed4bb5b/+5v",
|
||||
"cap_count": 6,
|
||||
"expected": {},
|
||||
"simulated": {
|
||||
"z_min_ohms": 0.0078,
|
||||
"z_at_1MHz_ohms": 0.0093,
|
||||
"z_at_100kHz_ohms": 0.0781
|
||||
},
|
||||
"delta": {},
|
||||
"model_note": "generic ESR + parasitic L from self-resonant frequency",
|
||||
"status": "pass",
|
||||
"note": "Z=0.009\u03a9 at 1MHz",
|
||||
"elapsed_s": 0.016,
|
||||
"reference": "C18/C9/C12/C6/C4"
|
||||
},
|
||||
{
|
||||
"subcircuit_type": "decoupling",
|
||||
"components": [
|
||||
"C2"
|
||||
],
|
||||
"rail": "+5v",
|
||||
"cap_count": 1,
|
||||
"expected": {},
|
||||
"simulated": {
|
||||
"z_min_ohms": 0.0158,
|
||||
"f_at_zmin_hz": 1678740.0,
|
||||
"z_at_1MHz_ohms": 0.0188,
|
||||
"z_at_100kHz_ohms": 0.1594
|
||||
},
|
||||
"delta": {},
|
||||
"model_note": "generic ESR + parasitic L from self-resonant frequency",
|
||||
"status": "pass",
|
||||
"note": "Z=0.019\u03a9 at 1MHz",
|
||||
"elapsed_s": 0.013,
|
||||
"reference": "C2"
|
||||
},
|
||||
{
|
||||
"subcircuit_type": "decoupling",
|
||||
"components": [
|
||||
"C7",
|
||||
"C3",
|
||||
"C8"
|
||||
],
|
||||
"rail": "VBUS",
|
||||
"cap_count": 3,
|
||||
"expected": {},
|
||||
"simulated": {
|
||||
"z_min_ohms": 0.0075,
|
||||
"z_at_1MHz_ohms": 0.009,
|
||||
"z_at_100kHz_ohms": 0.0569
|
||||
},
|
||||
"delta": {},
|
||||
"model_note": "generic ESR + parasitic L from self-resonant frequency",
|
||||
"status": "pass",
|
||||
"note": "Z=0.009\u03a9 at 1MHz",
|
||||
"elapsed_s": 0.013,
|
||||
"reference": "C7/C3/C8"
|
||||
},
|
||||
{
|
||||
"subcircuit_type": "regulator_feedback",
|
||||
"components": [
|
||||
"R15",
|
||||
"R16"
|
||||
],
|
||||
"regulator": "U8",
|
||||
"expected": {
|
||||
"ratio": 0.180328,
|
||||
"vref_V": 0.6,
|
||||
"vfb_V": 0.599951256,
|
||||
"vin_V": 3.327
|
||||
},
|
||||
"simulated": {
|
||||
"vfb_V": 0.6
|
||||
},
|
||||
"delta": {
|
||||
"vfb_error_pct": 0.0
|
||||
},
|
||||
"status": "pass",
|
||||
"elapsed_s": 0.01,
|
||||
"reference": "R15/R16"
|
||||
},
|
||||
{
|
||||
"subcircuit_type": "inrush",
|
||||
"components": [
|
||||
"C7",
|
||||
"C3",
|
||||
"C8"
|
||||
],
|
||||
"regulator": "U6",
|
||||
"expected": {
|
||||
"v_target_V": 5.0,
|
||||
"estimated_inrush_A": 1.2
|
||||
},
|
||||
"simulated": {
|
||||
"v_settled_V": 5.0
|
||||
},
|
||||
"delta": {
|
||||
"v_settle_error_pct": 0.0
|
||||
},
|
||||
"status": "pass",
|
||||
"note": "Transient simulated",
|
||||
"elapsed_s": 0.013,
|
||||
"reference": "C7/C3/C8"
|
||||
},
|
||||
{
|
||||
"subcircuit_type": "inrush",
|
||||
"components": [
|
||||
"C16",
|
||||
"C19",
|
||||
"C14",
|
||||
"C15",
|
||||
"C17"
|
||||
],
|
||||
"regulator": "U8",
|
||||
"expected": {
|
||||
"v_target_V": 3.327,
|
||||
"estimated_inrush_A": 0.133
|
||||
},
|
||||
"simulated": {
|
||||
"v_settled_V": 3.327
|
||||
},
|
||||
"delta": {
|
||||
"v_settle_error_pct": 0.0
|
||||
},
|
||||
"status": "pass",
|
||||
"note": "Transient simulated",
|
||||
"elapsed_s": 0.011,
|
||||
"reference": "C16/C19/C14/C15/C17"
|
||||
}
|
||||
],
|
||||
"trust_summary": {
|
||||
"total_findings": 0,
|
||||
"trust_level": "high",
|
||||
"by_confidence": {
|
||||
"deterministic": 0,
|
||||
"heuristic": 0,
|
||||
"datasheet-backed": 0
|
||||
},
|
||||
"by_evidence_source": {
|
||||
"datasheet": 0,
|
||||
"topology": 0,
|
||||
"heuristic_rule": 0,
|
||||
"symbol_footprint": 0,
|
||||
"bom": 0,
|
||||
"geometry": 0,
|
||||
"api_lookup": 0
|
||||
},
|
||||
"provenance_coverage_pct": null
|
||||
},
|
||||
"total_elapsed_s": 1.31,
|
||||
"simulator": "/opt/homebrew/bin/ngspice",
|
||||
"ngspice": "/opt/homebrew/bin/ngspice"
|
||||
}
|
||||
38
analysis/spice_runs/decoupling_C18_C9_C12_C6_C4.cir
Normal file
38
analysis/spice_runs/decoupling_C18_C9_C12_C6_C4.cir
Normal file
@@ -0,0 +1,38 @@
|
||||
* Auto-generated testbench for decoupling analysis: /00000000-0000-0000-0000-00005ed4bb5b/+5v rail
|
||||
* 6 capacitor(s) in parallel, series R-L-C model
|
||||
* Model: ESR/ESL from pdn_impedance (package-based)
|
||||
|
||||
R_esr_C18 rail n_esr_0 948.7m
|
||||
L_par_C18 n_esr_0 n_lpar_0 500p
|
||||
C18 n_lpar_0 0 100n
|
||||
R_esr_C9 rail n_esr_1 948.7m
|
||||
L_par_C9 n_esr_1 n_lpar_1 500p
|
||||
C9 n_lpar_1 0 100n
|
||||
R_esr_C12 rail n_esr_2 948.7m
|
||||
L_par_C12 n_esr_2 n_lpar_2 500p
|
||||
C12 n_lpar_2 0 100n
|
||||
R_esr_C6 rail n_esr_3 15.8m
|
||||
L_par_C6 n_esr_3 n_lpar_3 900p
|
||||
C6 n_lpar_3 0 10u
|
||||
R_esr_C4 rail n_esr_4 15.8m
|
||||
L_par_C4 n_esr_4 n_lpar_4 900p
|
||||
C4 n_lpar_4 0 10u
|
||||
R_esr_C10 rail n_esr_5 948.7m
|
||||
L_par_C10 n_esr_5 n_lpar_5 500p
|
||||
C10 n_lpar_5 0 100n
|
||||
|
||||
* 1A AC current source into the rail node — V(rail) = Z(f)
|
||||
IAC 0 rail DC 0 AC 1
|
||||
|
||||
.control
|
||||
ac dec 200 100 100Meg
|
||||
let z_mag = vm(rail)
|
||||
meas ac z_min min z_mag
|
||||
meas ac f_at_zmin when z_mag=z_min
|
||||
meas ac z_at_1M find vm(rail) at=1Meg
|
||||
meas ac z_at_100k find vm(rail) at=100k
|
||||
echo "z_min=$&z_min f_at_zmin=$&f_at_zmin z_at_1M=$&z_at_1M z_at_100k=$&z_at_100k n_caps=6" > analysis/spice_runs/decoupling_C18_C9_C12_C6_C4.out
|
||||
quit
|
||||
.endc
|
||||
|
||||
.end
|
||||
24
analysis/spice_runs/decoupling_C18_C9_C12_C6_C4.log
Normal file
24
analysis/spice_runs/decoupling_C18_C9_C12_C6_C4.log
Normal file
@@ -0,0 +1,24 @@
|
||||
=== stdout ===
|
||||
|
||||
Note: No compatibility mode selected!
|
||||
|
||||
|
||||
Circuit: * auto-generated testbench for decoupling analysis: /00000000-0000-0000-0000-00005ed4bb5b/+5v rail
|
||||
|
||||
Doing analysis at TEMP = 27.000000 and TNOM = 27.000000
|
||||
|
||||
Using SPARSE 1.3 as Direct Linear Solver
|
||||
|
||||
No. of Data Rows : 1201
|
||||
z_min = 7.76885e-03 at= 1.67880e+06
|
||||
meas ac f_at_zmin when z_mag=7.768850e-03 failed!
|
||||
|
||||
z_at_1m = 9.25020e-03
|
||||
z_at_100k = 7.81266e-02
|
||||
ngspice-46 done
|
||||
|
||||
=== stderr ===
|
||||
|
||||
Error: measure f_at_zmin when(WHEN) : out of interval
|
||||
Error: &f_at_zmin: no such variable.
|
||||
|
||||
1
analysis/spice_runs/decoupling_C18_C9_C12_C6_C4.out
Normal file
1
analysis/spice_runs/decoupling_C18_C9_C12_C6_C4.out
Normal file
@@ -0,0 +1 @@
|
||||
z_min=0.00776885 f_at_zmin= z_at_1M=0.0092502 z_at_100k=0.0781266 n_caps=6
|
||||
23
analysis/spice_runs/decoupling_C2.cir
Normal file
23
analysis/spice_runs/decoupling_C2.cir
Normal file
@@ -0,0 +1,23 @@
|
||||
* Auto-generated testbench for decoupling analysis: +5v rail
|
||||
* 1 capacitor(s) in parallel, series R-L-C model
|
||||
* Model: ESR/ESL from pdn_impedance (package-based)
|
||||
|
||||
R_esr_C2 rail n_esr_0 15.8m
|
||||
L_par_C2 n_esr_0 n_lpar_0 900p
|
||||
C2 n_lpar_0 0 10u
|
||||
|
||||
* 1A AC current source into the rail node — V(rail) = Z(f)
|
||||
IAC 0 rail DC 0 AC 1
|
||||
|
||||
.control
|
||||
ac dec 200 100 100Meg
|
||||
let z_mag = vm(rail)
|
||||
meas ac z_min min z_mag
|
||||
meas ac f_at_zmin when z_mag=z_min
|
||||
meas ac z_at_1M find vm(rail) at=1Meg
|
||||
meas ac z_at_100k find vm(rail) at=100k
|
||||
echo "z_min=$&z_min f_at_zmin=$&f_at_zmin z_at_1M=$&z_at_1M z_at_100k=$&z_at_100k n_caps=1" > analysis/spice_runs/decoupling_C2.out
|
||||
quit
|
||||
.endc
|
||||
|
||||
.end
|
||||
40
analysis/spice_runs/decoupling_C2.log
Normal file
40
analysis/spice_runs/decoupling_C2.log
Normal file
@@ -0,0 +1,40 @@
|
||||
=== stdout ===
|
||||
|
||||
Note: No compatibility mode selected!
|
||||
|
||||
|
||||
Circuit: * auto-generated testbench for decoupling analysis: +5v rail
|
||||
|
||||
Doing analysis at TEMP = 27.000000 and TNOM = 27.000000
|
||||
|
||||
Using SPARSE 1.3 as Direct Linear Solver
|
||||
|
||||
No. of Data Rows : 1201
|
||||
z_min = 1.58000e-02 at= 1.67880e+06
|
||||
f_at_zmin = 1.67874e+06
|
||||
z_at_1m = 1.88393e-02
|
||||
z_at_100k = 1.59375e-01
|
||||
ngspice-46 done
|
||||
|
||||
=== stderr ===
|
||||
Warning: singular matrix: check node n_lpar_0
|
||||
|
||||
Note: Starting dynamic gmin stepping
|
||||
Warning: singular matrix: check node n_lpar_0
|
||||
|
||||
Warning: Dynamic gmin stepping failed
|
||||
Note: Starting true gmin stepping
|
||||
Warning: singular matrix: check node n_lpar_0
|
||||
|
||||
Warning: singular matrix: check node n_lpar_0
|
||||
|
||||
Warning: singular matrix: check node n_lpar_0
|
||||
|
||||
Warning: singular matrix: check node n_lpar_0
|
||||
|
||||
Warning: True gmin stepping failed
|
||||
Note: Starting source stepping
|
||||
Warning: source stepping failed
|
||||
Note: Transient op started
|
||||
Note: Transient op finished successfully
|
||||
|
||||
1
analysis/spice_runs/decoupling_C2.out
Normal file
1
analysis/spice_runs/decoupling_C2.out
Normal file
@@ -0,0 +1 @@
|
||||
z_min=0.0158 f_at_zmin=1.67874E+06 z_at_1M=0.0188393 z_at_100k=0.159375 n_caps=1
|
||||
29
analysis/spice_runs/decoupling_C7_C3_C8.cir
Normal file
29
analysis/spice_runs/decoupling_C7_C3_C8.cir
Normal file
@@ -0,0 +1,29 @@
|
||||
* Auto-generated testbench for decoupling analysis: VBUS rail
|
||||
* 3 capacitor(s) in parallel, series R-L-C model
|
||||
* Model: ESR/ESL from pdn_impedance (package-based)
|
||||
|
||||
R_esr_C7 rail n_esr_0 15.8m
|
||||
L_par_C7 n_esr_0 n_lpar_0 900p
|
||||
C7 n_lpar_0 0 10u
|
||||
R_esr_C3 rail n_esr_1 100m
|
||||
L_par_C3 n_esr_1 n_lpar_1 7.5n
|
||||
C3 n_lpar_1 0 100u
|
||||
R_esr_C8 rail n_esr_2 15.8m
|
||||
L_par_C8 n_esr_2 n_lpar_2 900p
|
||||
C8 n_lpar_2 0 10u
|
||||
|
||||
* 1A AC current source into the rail node — V(rail) = Z(f)
|
||||
IAC 0 rail DC 0 AC 1
|
||||
|
||||
.control
|
||||
ac dec 200 100 100Meg
|
||||
let z_mag = vm(rail)
|
||||
meas ac z_min min z_mag
|
||||
meas ac f_at_zmin when z_mag=z_min
|
||||
meas ac z_at_1M find vm(rail) at=1Meg
|
||||
meas ac z_at_100k find vm(rail) at=100k
|
||||
echo "z_min=$&z_min f_at_zmin=$&f_at_zmin z_at_1M=$&z_at_1M z_at_100k=$&z_at_100k n_caps=3" > analysis/spice_runs/decoupling_C7_C3_C8.out
|
||||
quit
|
||||
.endc
|
||||
|
||||
.end
|
||||
44
analysis/spice_runs/decoupling_C7_C3_C8.log
Normal file
44
analysis/spice_runs/decoupling_C7_C3_C8.log
Normal file
@@ -0,0 +1,44 @@
|
||||
=== stdout ===
|
||||
|
||||
Note: No compatibility mode selected!
|
||||
|
||||
|
||||
Circuit: * auto-generated testbench for decoupling analysis: vbus rail
|
||||
|
||||
Doing analysis at TEMP = 27.000000 and TNOM = 27.000000
|
||||
|
||||
Using SPARSE 1.3 as Direct Linear Solver
|
||||
|
||||
No. of Data Rows : 1201
|
||||
z_min = 7.52546e-03 at= 1.67880e+06
|
||||
meas ac f_at_zmin when z_mag=7.525463e-03 failed!
|
||||
|
||||
z_at_1m = 8.98325e-03
|
||||
z_at_100k = 5.69392e-02
|
||||
ngspice-46 done
|
||||
|
||||
=== stderr ===
|
||||
Warning: singular matrix: check node n_lpar_1
|
||||
|
||||
Note: Starting dynamic gmin stepping
|
||||
Warning: singular matrix: check node rail
|
||||
|
||||
Warning: Dynamic gmin stepping failed
|
||||
Note: Starting true gmin stepping
|
||||
Warning: singular matrix: check node rail
|
||||
|
||||
Warning: singular matrix: check node rail
|
||||
|
||||
Warning: singular matrix: check node rail
|
||||
|
||||
Warning: singular matrix: check node rail
|
||||
|
||||
Warning: True gmin stepping failed
|
||||
Note: Starting source stepping
|
||||
Warning: source stepping failed
|
||||
Note: Transient op started
|
||||
Note: Transient op finished successfully
|
||||
|
||||
Error: measure f_at_zmin when(WHEN) : out of interval
|
||||
Error: &f_at_zmin: no such variable.
|
||||
|
||||
1
analysis/spice_runs/decoupling_C7_C3_C8.out
Normal file
1
analysis/spice_runs/decoupling_C7_C3_C8.out
Normal file
@@ -0,0 +1 @@
|
||||
z_min=0.00752546 f_at_zmin= z_at_1M=0.00898326 z_at_100k=0.0569392 n_caps=3
|
||||
25
analysis/spice_runs/inrush_idx0.cir
Normal file
25
analysis/spice_runs/inrush_idx0.cir
Normal file
@@ -0,0 +1,25 @@
|
||||
* Auto-generated testbench for inrush analysis: U6 (LDO)
|
||||
* Output: 5.0V, 120.0µF total capacitance
|
||||
* Soft-start: 0.5ms ramp
|
||||
|
||||
* Voltage source with linear ramp (soft-start) — 10us delay before ramp
|
||||
Vreg ramp 0 PWL(0 0 10u 0 510u 5.0)
|
||||
* Regulator output impedance
|
||||
Rout ramp out 100m
|
||||
|
||||
* Output capacitors
|
||||
C7 out 0 10u
|
||||
C3 out 0 100u
|
||||
C8 out 0 10u
|
||||
|
||||
.control
|
||||
tran 1u 1.5m
|
||||
let i_out = abs(i(Rout))
|
||||
let i_peak = vecmax(i_out)
|
||||
let t_peak = i_out[vecmin(abs(i_out - i_peak))]
|
||||
meas tran v_settled find v(out) at=1.35m
|
||||
echo "i_peak=$&i_peak t_peak=$&t_peak v_settled=$&v_settled v_target=5.0 r_out=0.1" > analysis/spice_runs/inrush_idx0.out
|
||||
quit
|
||||
.endc
|
||||
|
||||
.end
|
||||
37
analysis/spice_runs/inrush_idx0.log
Normal file
37
analysis/spice_runs/inrush_idx0.log
Normal file
@@ -0,0 +1,37 @@
|
||||
=== stdout ===
|
||||
|
||||
Note: No compatibility mode selected!
|
||||
|
||||
|
||||
Circuit: * auto-generated testbench for inrush analysis: u6 (ldo)
|
||||
|
||||
Doing analysis at TEMP = 27.000000 and TNOM = 27.000000
|
||||
|
||||
Using SPARSE 1.3 as Direct Linear Solver
|
||||
|
||||
Initial Transient Solution
|
||||
--------------------------
|
||||
|
||||
Node Voltage
|
||||
---- -------
|
||||
ramp 0
|
||||
out 0
|
||||
vreg#branch 0
|
||||
|
||||
|
||||
No. of Data Rows : 1517
|
||||
v_settled = 5.00000e+00
|
||||
ngspice-46 done
|
||||
|
||||
=== stderr ===
|
||||
|
||||
Error: no such function as i,
|
||||
or i(rout) is not available.
|
||||
Error: RHS "abs(i(rout))" invalid
|
||||
Warning from checkvalid: vector i_out is not available or has zero length.
|
||||
Error: RHS "vecmax(i_out)" invalid
|
||||
Warning from checkvalid: vector i_out is not available or has zero length.
|
||||
Error: RHS "i_out[vecmin(abs(i_out - i_peak))]" invalid
|
||||
Error: &i_peak: no such variable.
|
||||
Error: &t_peak: no such variable.
|
||||
|
||||
1
analysis/spice_runs/inrush_idx0.out
Normal file
1
analysis/spice_runs/inrush_idx0.out
Normal file
@@ -0,0 +1 @@
|
||||
i_peak= t_peak= v_settled=5 v_target=5.0 r_out=0.1
|
||||
27
analysis/spice_runs/inrush_idx1.cir
Normal file
27
analysis/spice_runs/inrush_idx1.cir
Normal file
@@ -0,0 +1,27 @@
|
||||
* Auto-generated testbench for inrush analysis: U8 (switching)
|
||||
* Output: 3.327V, 40.1µF total capacitance
|
||||
* Soft-start: 1.0ms ramp
|
||||
|
||||
* Voltage source with linear ramp (soft-start) — 10us delay before ramp
|
||||
Vreg ramp 0 PWL(0 0 10u 0 1.01m 3.327)
|
||||
* Regulator output impedance
|
||||
Rout ramp out 10m
|
||||
|
||||
* Output capacitors
|
||||
C16 out 0 10u
|
||||
C19 out 0 100n
|
||||
C14 out 0 10u
|
||||
C15 out 0 10u
|
||||
C17 out 0 10u
|
||||
|
||||
.control
|
||||
tran 2u 3m
|
||||
let i_out = abs(i(Rout))
|
||||
let i_peak = vecmax(i_out)
|
||||
let t_peak = i_out[vecmin(abs(i_out - i_peak))]
|
||||
meas tran v_settled find v(out) at=2.7m
|
||||
echo "i_peak=$&i_peak t_peak=$&t_peak v_settled=$&v_settled v_target=3.327 r_out=0.01" > analysis/spice_runs/inrush_idx1.out
|
||||
quit
|
||||
.endc
|
||||
|
||||
.end
|
||||
37
analysis/spice_runs/inrush_idx1.log
Normal file
37
analysis/spice_runs/inrush_idx1.log
Normal file
@@ -0,0 +1,37 @@
|
||||
=== stdout ===
|
||||
|
||||
Note: No compatibility mode selected!
|
||||
|
||||
|
||||
Circuit: * auto-generated testbench for inrush analysis: u8 (switching)
|
||||
|
||||
Doing analysis at TEMP = 27.000000 and TNOM = 27.000000
|
||||
|
||||
Using SPARSE 1.3 as Direct Linear Solver
|
||||
|
||||
Initial Transient Solution
|
||||
--------------------------
|
||||
|
||||
Node Voltage
|
||||
---- -------
|
||||
ramp 0
|
||||
out 0
|
||||
vreg#branch 0
|
||||
|
||||
|
||||
No. of Data Rows : 1518
|
||||
v_settled = 3.32700e+00
|
||||
ngspice-46 done
|
||||
|
||||
=== stderr ===
|
||||
|
||||
Error: no such function as i,
|
||||
or i(rout) is not available.
|
||||
Error: RHS "abs(i(rout))" invalid
|
||||
Warning from checkvalid: vector i_out is not available or has zero length.
|
||||
Error: RHS "vecmax(i_out)" invalid
|
||||
Warning from checkvalid: vector i_out is not available or has zero length.
|
||||
Error: RHS "i_out[vecmin(abs(i_out - i_peak))]" invalid
|
||||
Error: &i_peak: no such variable.
|
||||
Error: &t_peak: no such variable.
|
||||
|
||||
1
analysis/spice_runs/inrush_idx1.out
Normal file
1
analysis/spice_runs/inrush_idx1.out
Normal file
@@ -0,0 +1 @@
|
||||
i_peak= t_peak= v_settled=3.327 v_target=3.327 r_out=0.01
|
||||
20
analysis/spice_runs/regulator-feedback_U8_R15_R16.cir
Normal file
20
analysis/spice_runs/regulator-feedback_U8_R15_R16.cir
Normal file
@@ -0,0 +1,20 @@
|
||||
* Auto-generated testbench for regulator feedback: U8
|
||||
* Divider: R15/R16, ratio=0.1803
|
||||
* Expected Vout=3.327V, Vfb=0.6000V (Vref=0.6V)
|
||||
* Topology: standard
|
||||
* Model: ideal passives (exact for DC divider)
|
||||
|
||||
VIN out_rail 0 DC 3.327
|
||||
R15 out_rail fb_net 10k
|
||||
R16 fb_net 0 2.2k
|
||||
|
||||
.control
|
||||
op
|
||||
let vfb_sim = v(fb_net)
|
||||
let expected = 0.599951256
|
||||
let error_pct = abs(vfb_sim - expected) / expected * 100
|
||||
echo "vfb_sim=$&vfb_sim error_pct=$&error_pct expected=0.599951256 vin=3.327 vref=0.6" > analysis/spice_runs/regulator-feedback_U8_R15_R16.out
|
||||
quit
|
||||
.endc
|
||||
|
||||
.end
|
||||
16
analysis/spice_runs/regulator-feedback_U8_R15_R16.log
Normal file
16
analysis/spice_runs/regulator-feedback_U8_R15_R16.log
Normal file
@@ -0,0 +1,16 @@
|
||||
=== stdout ===
|
||||
|
||||
Note: No compatibility mode selected!
|
||||
|
||||
|
||||
Circuit: * auto-generated testbench for regulator feedback: u8
|
||||
|
||||
Doing analysis at TEMP = 27.000000 and TNOM = 27.000000
|
||||
|
||||
Using SPARSE 1.3 as Direct Linear Solver
|
||||
|
||||
No. of Data Rows : 1
|
||||
ngspice-46 done
|
||||
|
||||
=== stderr ===
|
||||
|
||||
1
analysis/spice_runs/regulator-feedback_U8_R15_R16.out
Normal file
1
analysis/spice_runs/regulator-feedback_U8_R15_R16.out
Normal file
@@ -0,0 +1 @@
|
||||
vfb_sim=0.599951 error_pct=7.27272E-05 expected=0.599951256 vin=3.327 vref=0.6
|
||||
17
analysis/spice_runs/voltage-divider_R15_R16.cir
Normal file
17
analysis/spice_runs/voltage-divider_R15_R16.cir
Normal file
@@ -0,0 +1,17 @@
|
||||
* Auto-generated testbench for voltage divider: R15/R16
|
||||
* Expected Vout = 3.3 * 0.180328 = 0.5951 V
|
||||
* Model: ideal passives (exact, unloaded)
|
||||
|
||||
VIN M2_3v3 0 DC 3.3
|
||||
R15 M2_3v3 FB 10k
|
||||
R16 FB 0 2.2k
|
||||
|
||||
.control
|
||||
op
|
||||
let vout_sim = v(FB)
|
||||
let error_pct = abs(vout_sim - 0.5950823999999999) / 0.5950823999999999 * 100
|
||||
echo "vout_sim=$&vout_sim error_pct=$&error_pct expected=0.5950823999999999" > analysis/spice_runs/voltage-divider_R15_R16.out
|
||||
quit
|
||||
.endc
|
||||
|
||||
.end
|
||||
16
analysis/spice_runs/voltage-divider_R15_R16.log
Normal file
16
analysis/spice_runs/voltage-divider_R15_R16.log
Normal file
@@ -0,0 +1,16 @@
|
||||
=== stdout ===
|
||||
|
||||
Note: No compatibility mode selected!
|
||||
|
||||
|
||||
Circuit: * auto-generated testbench for voltage divider: r15/r16
|
||||
|
||||
Doing analysis at TEMP = 27.000000 and TNOM = 27.000000
|
||||
|
||||
Using SPARSE 1.3 as Direct Linear Solver
|
||||
|
||||
No. of Data Rows : 1
|
||||
ngspice-46 done
|
||||
|
||||
=== stderr ===
|
||||
|
||||
1
analysis/spice_runs/voltage-divider_R15_R16.out
Normal file
1
analysis/spice_runs/voltage-divider_R15_R16.out
Normal file
@@ -0,0 +1 @@
|
||||
vout_sim=0.595082 error_pct=7.27272E-05 expected=0.5950823999999999
|
||||
851
carrier/CM5IO.kicad_pro
Normal file
851
carrier/CM5IO.kicad_pro
Normal file
@@ -0,0 +1,851 @@
|
||||
{
|
||||
"board": {
|
||||
"3dviewports": [],
|
||||
"design_settings": {
|
||||
"defaults": {
|
||||
"apply_defaults_to_fp_fields": false,
|
||||
"apply_defaults_to_fp_shapes": false,
|
||||
"apply_defaults_to_fp_text": false,
|
||||
"board_outline_line_width": 0.05,
|
||||
"copper_line_width": 0.2,
|
||||
"copper_text_italic": false,
|
||||
"copper_text_size_h": 1.5,
|
||||
"copper_text_size_v": 1.5,
|
||||
"copper_text_thickness": 0.3,
|
||||
"copper_text_upright": false,
|
||||
"courtyard_line_width": 0.05,
|
||||
"dimension_precision": 1,
|
||||
"dimension_units": 2,
|
||||
"dimensions": {
|
||||
"arrow_length": 1270000,
|
||||
"extension_offset": 500000,
|
||||
"keep_text_aligned": true,
|
||||
"suppress_zeroes": false,
|
||||
"text_position": 0,
|
||||
"units_format": 1
|
||||
},
|
||||
"fab_line_width": 0.1,
|
||||
"fab_text_italic": false,
|
||||
"fab_text_size_h": 1.0,
|
||||
"fab_text_size_v": 1.0,
|
||||
"fab_text_thickness": 0.15,
|
||||
"fab_text_upright": false,
|
||||
"other_line_width": 0.1,
|
||||
"other_text_italic": false,
|
||||
"other_text_size_h": 1.0,
|
||||
"other_text_size_v": 1.0,
|
||||
"other_text_thickness": 0.15,
|
||||
"other_text_upright": false,
|
||||
"pads": {
|
||||
"drill": 2.7,
|
||||
"height": 2.7,
|
||||
"width": 2.7
|
||||
},
|
||||
"silk_line_width": 0.12,
|
||||
"silk_text_italic": false,
|
||||
"silk_text_size_h": 1.0,
|
||||
"silk_text_size_v": 1.0,
|
||||
"silk_text_thickness": 0.15,
|
||||
"silk_text_upright": false,
|
||||
"zones": {
|
||||
"45_degree_only": false,
|
||||
"min_clearance": 0.39
|
||||
}
|
||||
},
|
||||
"diff_pair_dimensions": [
|
||||
{
|
||||
"gap": 0.0,
|
||||
"via_gap": 0.0,
|
||||
"width": 0.0
|
||||
}
|
||||
],
|
||||
"drc_exclusions": [
|
||||
"clearance|139230000|150500000|44050c2e-137a-4ee5-aab6-af6e82cb550c|c1309054-b3ec-4f1e-9334-00cba6baa146",
|
||||
"clearance|140500000|149230000|fcebfa8a-eb3d-4196-9404-532dc9ddde9f|c1309054-b3ec-4f1e-9334-00cba6baa146",
|
||||
"clearance|140920000|151770000|40ece557-24f2-47a4-bf57-058580957d62|44050c2e-137a-4ee5-aab6-af6e82cb550c",
|
||||
"clearance|141770000|150920000|40ece557-24f2-47a4-bf57-058580957d62|fcebfa8a-eb3d-4196-9404-532dc9ddde9f",
|
||||
"clearance|146205000|147060000|cf9a04ac-e742-4cc4-b820-74b94e875632|fd267553-ab4f-45b2-b9de-3fd8c755208d",
|
||||
"clearance|159795000|147060000|386d8066-f8d7-448e-8535-fb964d94eab0|49b00fb2-fbf7-4862-ac04-916c407cd0f2",
|
||||
"clearance|159975000|145410000|029ebbb9-b02f-45bd-96ed-01f4d0ed11f9|8cfd9587-a6c9-4a80-b337-75a1568e78eb",
|
||||
"clearance|163003858|146776424|a2c51ef8-4a45-4b19-8a83-e2fa4ed59648|49b00fb2-fbf7-4862-ac04-916c407cd0f2",
|
||||
"courtyards_overlap|124749999|104420001|00000000-0000-0000-0000-00005d2e9306|00000000-0000-0000-0000-00005e42b6ed",
|
||||
"footprint_type_mismatch|187000000|157525000|00000000-0000-0000-0000-00005d2fec1a|00000000-0000-0000-0000-000000000000"
|
||||
],
|
||||
"meta": {
|
||||
"version": 2
|
||||
},
|
||||
"rule_severities": {
|
||||
"annular_width": "error",
|
||||
"clearance": "warning",
|
||||
"connection_width": "warning",
|
||||
"copper_edge_clearance": "error",
|
||||
"copper_sliver": "warning",
|
||||
"courtyards_overlap": "error",
|
||||
"diff_pair_gap_out_of_range": "warning",
|
||||
"diff_pair_uncoupled_length_too_long": "error",
|
||||
"drill_out_of_range": "error",
|
||||
"duplicate_footprints": "warning",
|
||||
"extra_footprint": "warning",
|
||||
"footprint": "error",
|
||||
"footprint_symbol_mismatch": "warning",
|
||||
"footprint_type_mismatch": "error",
|
||||
"hole_clearance": "error",
|
||||
"hole_near_hole": "error",
|
||||
"holes_co_located": "warning",
|
||||
"invalid_outline": "error",
|
||||
"isolated_copper": "warning",
|
||||
"item_on_disabled_layer": "error",
|
||||
"items_not_allowed": "error",
|
||||
"length_out_of_range": "error",
|
||||
"lib_footprint_issues": "warning",
|
||||
"lib_footprint_mismatch": "warning",
|
||||
"malformed_courtyard": "error",
|
||||
"microvia_drill_out_of_range": "error",
|
||||
"missing_courtyard": "ignore",
|
||||
"missing_footprint": "warning",
|
||||
"net_conflict": "warning",
|
||||
"npth_inside_courtyard": "warning",
|
||||
"padstack": "error",
|
||||
"pth_inside_courtyard": "warning",
|
||||
"shorting_items": "error",
|
||||
"silk_edge_clearance": "ignore",
|
||||
"silk_over_copper": "error",
|
||||
"silk_overlap": "error",
|
||||
"skew_out_of_range": "error",
|
||||
"solder_mask_bridge": "ignore",
|
||||
"starved_thermal": "ignore",
|
||||
"text_height": "warning",
|
||||
"text_thickness": "warning",
|
||||
"through_hole_pad_without_hole": "error",
|
||||
"too_many_vias": "error",
|
||||
"track_dangling": "warning",
|
||||
"track_width": "error",
|
||||
"tracks_crossing": "error",
|
||||
"unconnected_items": "error",
|
||||
"unresolved_variable": "error",
|
||||
"via_dangling": "warning",
|
||||
"zones_intersect": "error"
|
||||
},
|
||||
"rules": {
|
||||
"allow_blind_buried_vias": false,
|
||||
"allow_microvias": false,
|
||||
"max_error": 0.005,
|
||||
"min_clearance": 0.125,
|
||||
"min_connection": 0.0,
|
||||
"min_copper_edge_clearance": 0.01,
|
||||
"min_hole_clearance": 0.19,
|
||||
"min_hole_to_hole": 0.24,
|
||||
"min_microvia_diameter": 0.2,
|
||||
"min_microvia_drill": 0.1,
|
||||
"min_resolved_spokes": 2,
|
||||
"min_silk_clearance": 0.0,
|
||||
"min_text_height": 0.8,
|
||||
"min_text_thickness": 0.08,
|
||||
"min_through_hole_diameter": 0.2,
|
||||
"min_track_width": 0.1,
|
||||
"min_via_annular_width": 0.1,
|
||||
"min_via_annulus": 0.049999999999999996,
|
||||
"min_via_diameter": 0.45,
|
||||
"solder_mask_to_copper_clearance": 0.0,
|
||||
"use_height_for_length_calcs": true
|
||||
},
|
||||
"teardrop_options": [
|
||||
{
|
||||
"td_onpadsmd": true,
|
||||
"td_onroundshapesonly": false,
|
||||
"td_ontrackend": false,
|
||||
"td_onviapad": true
|
||||
}
|
||||
],
|
||||
"teardrop_parameters": [
|
||||
{
|
||||
"td_allow_use_two_tracks": true,
|
||||
"td_curve_segcount": 0,
|
||||
"td_height_ratio": 1.0,
|
||||
"td_length_ratio": 0.5,
|
||||
"td_maxheight": 2.0,
|
||||
"td_maxlen": 1.0,
|
||||
"td_on_pad_in_zone": false,
|
||||
"td_target_name": "td_round_shape",
|
||||
"td_width_to_size_filter_ratio": 0.9
|
||||
},
|
||||
{
|
||||
"td_allow_use_two_tracks": true,
|
||||
"td_curve_segcount": 0,
|
||||
"td_height_ratio": 1.0,
|
||||
"td_length_ratio": 0.5,
|
||||
"td_maxheight": 2.0,
|
||||
"td_maxlen": 1.0,
|
||||
"td_on_pad_in_zone": false,
|
||||
"td_target_name": "td_rect_shape",
|
||||
"td_width_to_size_filter_ratio": 0.9
|
||||
},
|
||||
{
|
||||
"td_allow_use_two_tracks": true,
|
||||
"td_curve_segcount": 0,
|
||||
"td_height_ratio": 1.0,
|
||||
"td_length_ratio": 0.5,
|
||||
"td_maxheight": 2.0,
|
||||
"td_maxlen": 1.0,
|
||||
"td_on_pad_in_zone": false,
|
||||
"td_target_name": "td_track_end",
|
||||
"td_width_to_size_filter_ratio": 0.9
|
||||
}
|
||||
],
|
||||
"track_widths": [
|
||||
0.0,
|
||||
0.127,
|
||||
0.13,
|
||||
0.147,
|
||||
0.2,
|
||||
0.23,
|
||||
0.3,
|
||||
0.5,
|
||||
1.0,
|
||||
2.0,
|
||||
3.0
|
||||
],
|
||||
"tuning_pattern_settings": {
|
||||
"diff_pair_defaults": {
|
||||
"corner_radius_percentage": 100,
|
||||
"corner_style": 1,
|
||||
"max_amplitude": 1.0,
|
||||
"min_amplitude": 0.1,
|
||||
"single_sided": false,
|
||||
"spacing": 0.6
|
||||
},
|
||||
"diff_pair_skew_defaults": {
|
||||
"corner_radius_percentage": 100,
|
||||
"corner_style": 1,
|
||||
"max_amplitude": 1.0,
|
||||
"min_amplitude": 0.1,
|
||||
"single_sided": false,
|
||||
"spacing": 0.6
|
||||
},
|
||||
"single_track_defaults": {
|
||||
"corner_radius_percentage": 100,
|
||||
"corner_style": 1,
|
||||
"max_amplitude": 1.0,
|
||||
"min_amplitude": 0.1,
|
||||
"single_sided": false,
|
||||
"spacing": 0.6
|
||||
}
|
||||
},
|
||||
"via_dimensions": [
|
||||
{
|
||||
"diameter": 0.0,
|
||||
"drill": 0.0
|
||||
},
|
||||
{
|
||||
"diameter": 0.45,
|
||||
"drill": 0.2
|
||||
}
|
||||
],
|
||||
"zones_allow_external_fillets": false,
|
||||
"zones_use_no_outline": false
|
||||
},
|
||||
"ipc2581": {
|
||||
"dist": "",
|
||||
"distpn": "",
|
||||
"internal_id": "",
|
||||
"mfg": "",
|
||||
"mpn": ""
|
||||
},
|
||||
"layer_presets": [],
|
||||
"viewports": []
|
||||
},
|
||||
"boards": [],
|
||||
"cvpcb": {
|
||||
"equivalence_files": []
|
||||
},
|
||||
"erc": {
|
||||
"erc_exclusions": [
|
||||
"power_pin_not_driven|1003300|1181100|66cf9899-100d-45fb-9b9f-8f866de8a3fe|00000000-0000-0000-0000-000000000000|/e63e39d7-6ac0-4ffd-8aa3-1841a4541b55/00000000-0000-0000-0000-00005cff706a|/e63e39d7-6ac0-4ffd-8aa3-1841a4541b55/00000000-0000-0000-0000-00005cff706a|",
|
||||
"power_pin_not_driven|393700|1181100|0fc4267c-2119-444e-b3b2-d8a7bd88ec8a|00000000-0000-0000-0000-000000000000|/e63e39d7-6ac0-4ffd-8aa3-1841a4541b55/00000000-0000-0000-0000-00005cff706a|/e63e39d7-6ac0-4ffd-8aa3-1841a4541b55/00000000-0000-0000-0000-00005cff706a|"
|
||||
],
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"pin_map": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
],
|
||||
"rule_severities": {
|
||||
"bus_definition_conflict": "error",
|
||||
"bus_entry_needed": "error",
|
||||
"bus_to_bus_conflict": "error",
|
||||
"bus_to_net_conflict": "error",
|
||||
"conflicting_netclasses": "error",
|
||||
"different_unit_footprint": "error",
|
||||
"different_unit_net": "error",
|
||||
"duplicate_reference": "error",
|
||||
"duplicate_sheet_names": "error",
|
||||
"endpoint_off_grid": "warning",
|
||||
"extra_units": "error",
|
||||
"global_label_dangling": "error",
|
||||
"hier_label_mismatch": "error",
|
||||
"label_dangling": "error",
|
||||
"lib_symbol_issues": "warning",
|
||||
"missing_bidi_pin": "warning",
|
||||
"missing_input_pin": "warning",
|
||||
"missing_power_pin": "error",
|
||||
"missing_unit": "warning",
|
||||
"multiple_net_names": "error",
|
||||
"net_not_bus_member": "error",
|
||||
"no_connect_connected": "error",
|
||||
"no_connect_dangling": "error",
|
||||
"pin_not_connected": "error",
|
||||
"pin_not_driven": "error",
|
||||
"pin_to_pin": "error",
|
||||
"power_pin_not_driven": "error",
|
||||
"similar_labels": "error",
|
||||
"simulation_model_issue": "ignore",
|
||||
"unannotated": "error",
|
||||
"unit_value_mismatch": "error",
|
||||
"unresolved_variable": "error",
|
||||
"wire_dangling": "error"
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"pinned_footprint_libs": [],
|
||||
"pinned_symbol_libs": []
|
||||
},
|
||||
"meta": {
|
||||
"filename": "CM5IO.kicad_pro",
|
||||
"version": 1
|
||||
},
|
||||
"net_settings": {
|
||||
"classes": [
|
||||
{
|
||||
"bus_width": 12,
|
||||
"clearance": 0.125,
|
||||
"diff_pair_gap": 0.25,
|
||||
"diff_pair_via_gap": 0.25,
|
||||
"diff_pair_width": 0.13,
|
||||
"line_style": 0,
|
||||
"microvia_diameter": 0.3,
|
||||
"microvia_drill": 0.1,
|
||||
"name": "Default",
|
||||
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 0.13,
|
||||
"via_diameter": 0.45,
|
||||
"via_drill": 0.2,
|
||||
"wire_width": 6
|
||||
},
|
||||
{
|
||||
"bus_width": 12,
|
||||
"clearance": 0.13,
|
||||
"diff_pair_gap": 0.253,
|
||||
"diff_pair_via_gap": 0.25,
|
||||
"diff_pair_width": 0.127,
|
||||
"line_style": 0,
|
||||
"microvia_diameter": 0.3,
|
||||
"microvia_drill": 0.1,
|
||||
"name": "100R",
|
||||
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 0.127,
|
||||
"via_diameter": 0.45,
|
||||
"via_drill": 0.2,
|
||||
"wire_width": 6
|
||||
},
|
||||
{
|
||||
"bus_width": 12,
|
||||
"clearance": 0.13,
|
||||
"diff_pair_gap": 0.25,
|
||||
"diff_pair_via_gap": 0.25,
|
||||
"diff_pair_width": 0.178,
|
||||
"line_style": 0,
|
||||
"microvia_diameter": 0.3,
|
||||
"microvia_drill": 0.1,
|
||||
"name": "50R",
|
||||
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 0.13,
|
||||
"via_diameter": 0.45,
|
||||
"via_drill": 0.2,
|
||||
"wire_width": 6
|
||||
},
|
||||
{
|
||||
"bus_width": 12,
|
||||
"clearance": 0.13,
|
||||
"diff_pair_gap": 0.25,
|
||||
"diff_pair_via_gap": 0.25,
|
||||
"diff_pair_width": 0.13,
|
||||
"line_style": 0,
|
||||
"microvia_diameter": 0.3,
|
||||
"microvia_drill": 0.1,
|
||||
"name": "75R",
|
||||
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 0.13,
|
||||
"via_diameter": 0.45,
|
||||
"via_drill": 0.2,
|
||||
"wire_width": 6
|
||||
},
|
||||
{
|
||||
"bus_width": 12,
|
||||
"clearance": 0.13,
|
||||
"diff_pair_gap": 0.253,
|
||||
"diff_pair_via_gap": 0.25,
|
||||
"diff_pair_width": 0.147,
|
||||
"line_style": 0,
|
||||
"microvia_diameter": 0.3,
|
||||
"microvia_drill": 0.1,
|
||||
"name": "90R",
|
||||
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 0.147,
|
||||
"via_diameter": 0.45,
|
||||
"via_drill": 0.2,
|
||||
"wire_width": 6
|
||||
},
|
||||
{
|
||||
"bus_width": 12,
|
||||
"clearance": 0.13,
|
||||
"diff_pair_gap": 0.253,
|
||||
"diff_pair_via_gap": 0.25,
|
||||
"diff_pair_width": 0.127,
|
||||
"line_style": 0,
|
||||
"microvia_diameter": 0.3,
|
||||
"microvia_drill": 0.1,
|
||||
"name": "HDMI",
|
||||
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 0.127,
|
||||
"via_diameter": 0.45,
|
||||
"via_drill": 0.2,
|
||||
"wire_width": 6
|
||||
},
|
||||
{
|
||||
"bus_width": 12,
|
||||
"clearance": 1.0,
|
||||
"diff_pair_gap": 0.25,
|
||||
"diff_pair_via_gap": 0.25,
|
||||
"diff_pair_width": 0.13,
|
||||
"line_style": 0,
|
||||
"microvia_diameter": 0.3,
|
||||
"microvia_drill": 0.1,
|
||||
"name": "POE TAPS",
|
||||
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 0.3,
|
||||
"via_diameter": 0.45,
|
||||
"via_drill": 0.2,
|
||||
"wire_width": 6
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"version": 3
|
||||
},
|
||||
"net_colors": null,
|
||||
"netclass_assignments": null,
|
||||
"netclass_patterns": [
|
||||
{
|
||||
"netclass": "90R",
|
||||
"pattern": "/CM5_HighSpeed/USB3*"
|
||||
},
|
||||
{
|
||||
"netclass": "100R",
|
||||
"pattern": "/CM5_HighSpeed/DPHY*"
|
||||
},
|
||||
{
|
||||
"netclass": "HDMI",
|
||||
"pattern": "/CM5_HighSpeed/HDMI*_D*"
|
||||
},
|
||||
{
|
||||
"netclass": "HDMI",
|
||||
"pattern": "/CM5_HighSpeed/HDMI*_CK*"
|
||||
},
|
||||
{
|
||||
"netclass": "POE TAPS",
|
||||
"pattern": "/CM5_GPIO ( Ethernet, GPIO, SDCARD)/TR*_TAP"
|
||||
},
|
||||
{
|
||||
"netclass": "100R",
|
||||
"pattern": "/CM5_GPIO ( Ethernet, GPIO, SDCARD)/TRD*"
|
||||
},
|
||||
{
|
||||
"netclass": "90R",
|
||||
"pattern": "/CM5_HighSpeed/PCIE_*X_*"
|
||||
},
|
||||
{
|
||||
"netclass": "90R",
|
||||
"pattern": "/CM5_HighSpeed/PCIE_*CLK_*"
|
||||
},
|
||||
{
|
||||
"netclass": "90R",
|
||||
"pattern": "/USB2*"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pcbnew": {
|
||||
"last_paths": {
|
||||
"gencad": "CM5IOUSB3.cad",
|
||||
"idf": "",
|
||||
"netlist": "CM5IO.net",
|
||||
"plot": "./",
|
||||
"pos_files": "RPI-CM5IO-Gerber200924/",
|
||||
"specctra_dsn": "",
|
||||
"step": "",
|
||||
"svg": "SVG/",
|
||||
"vmrl": "",
|
||||
"vrml": ""
|
||||
},
|
||||
"page_layout_descr_file": ""
|
||||
},
|
||||
"schematic": {
|
||||
"annotate_start_num": 0,
|
||||
"bom_export_filename": "",
|
||||
"bom_fmt_presets": [],
|
||||
"bom_fmt_settings": {
|
||||
"field_delimiter": ",",
|
||||
"keep_line_breaks": false,
|
||||
"keep_tabs": false,
|
||||
"name": "CSV",
|
||||
"ref_delimiter": ",",
|
||||
"ref_range_delimiter": "",
|
||||
"string_delimiter": "\""
|
||||
},
|
||||
"bom_presets": [],
|
||||
"bom_settings": {
|
||||
"exclude_dnp": false,
|
||||
"fields_ordered": [
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Reference",
|
||||
"name": "Reference",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Value",
|
||||
"name": "Value",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Datasheet",
|
||||
"name": "Datasheet",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Footprint",
|
||||
"name": "Footprint",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Qty",
|
||||
"name": "${QUANTITY}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "DNP",
|
||||
"name": "${DNP}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "#",
|
||||
"name": "${ITEM_NUMBER}",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Field4",
|
||||
"name": "Field4",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Field5",
|
||||
"name": "Field5",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Field6",
|
||||
"name": "Field6",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Field7",
|
||||
"name": "Field7",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Field8",
|
||||
"name": "Field8",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Part Description",
|
||||
"name": "Part Description",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Description",
|
||||
"name": "Description",
|
||||
"show": false
|
||||
}
|
||||
],
|
||||
"filter_string": "",
|
||||
"group_symbols": true,
|
||||
"name": "",
|
||||
"sort_asc": true,
|
||||
"sort_field": "Reference"
|
||||
},
|
||||
"connection_grid_size": 50.0,
|
||||
"drawing": {
|
||||
"dashed_lines_dash_length_ratio": 12.0,
|
||||
"dashed_lines_gap_length_ratio": 3.0,
|
||||
"default_bus_thickness": 12.0,
|
||||
"default_junction_size": 40.0,
|
||||
"default_line_thickness": 6.0,
|
||||
"default_text_size": 50.0,
|
||||
"default_wire_thickness": 6.0,
|
||||
"field_names": [],
|
||||
"intersheets_ref_own_page": false,
|
||||
"intersheets_ref_prefix": "",
|
||||
"intersheets_ref_short": false,
|
||||
"intersheets_ref_show": false,
|
||||
"intersheets_ref_suffix": "",
|
||||
"junction_size_choice": 3,
|
||||
"label_size_ratio": 0.3,
|
||||
"operating_point_overlay_i_precision": 3,
|
||||
"operating_point_overlay_i_range": "~A",
|
||||
"operating_point_overlay_v_precision": 3,
|
||||
"operating_point_overlay_v_range": "~V",
|
||||
"overbar_offset_ratio": 1.23,
|
||||
"pin_symbol_size": 25.0,
|
||||
"text_offset_ratio": 0.3
|
||||
},
|
||||
"legacy_lib_dir": "",
|
||||
"legacy_lib_list": [],
|
||||
"meta": {
|
||||
"version": 1
|
||||
},
|
||||
"net_format_name": "",
|
||||
"ngspice": {
|
||||
"fix_include_paths": true,
|
||||
"fix_passive_vals": false,
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"model_mode": 0,
|
||||
"workbook_filename": ""
|
||||
},
|
||||
"page_layout_descr_file": "",
|
||||
"plot_directory": "./SVG",
|
||||
"spice_adjust_passive_values": false,
|
||||
"spice_current_sheet_as_root": false,
|
||||
"spice_external_command": "spice \"%I\"",
|
||||
"spice_model_current_sheet_as_root": true,
|
||||
"spice_save_all_currents": false,
|
||||
"spice_save_all_dissipations": false,
|
||||
"spice_save_all_voltages": false,
|
||||
"subpart_first_id": 65,
|
||||
"subpart_id_separator": 0
|
||||
},
|
||||
"sheets": [
|
||||
[
|
||||
"e63e39d7-6ac0-4ffd-8aa3-1841a4541b55",
|
||||
"Root"
|
||||
],
|
||||
[
|
||||
"00000000-0000-0000-0000-00005cff70b1",
|
||||
"CM5_HighSpeed"
|
||||
],
|
||||
[
|
||||
"00000000-0000-0000-0000-00005cff706a",
|
||||
"CM5_GPIO ( Ethernet, GPIO, SDCARD)"
|
||||
],
|
||||
[
|
||||
"c57f5f03-e2c5-5c1b-ba90-8072a2a088a0",
|
||||
"M2_Ekey"
|
||||
],
|
||||
[
|
||||
"9c7ccf72-dd3e-5ed3-bb43-2939833eb069",
|
||||
"Power"
|
||||
],
|
||||
[
|
||||
"5efffb63-edbb-5986-86d3-84ead412bc65",
|
||||
"GPS_PPS"
|
||||
]
|
||||
],
|
||||
"text_variables": {}
|
||||
}
|
||||
3302
carrier/CM5IO.kicad_sch
Normal file
3302
carrier/CM5IO.kicad_sch
Normal file
File diff suppressed because it is too large
Load Diff
10334
carrier/CM5IO.kicad_sym
Normal file
10334
carrier/CM5IO.kicad_sym
Normal file
File diff suppressed because it is too large
Load Diff
19662
carrier/CM5_GPIO.kicad_sch
Normal file
19662
carrier/CM5_GPIO.kicad_sch
Normal file
File diff suppressed because it is too large
Load Diff
12340
carrier/CM5_HighSpeed.kicad_sch
Normal file
12340
carrier/CM5_HighSpeed.kicad_sch
Normal file
File diff suppressed because it is too large
Load Diff
1453
carrier/GPS_PPS.kicad_sch
Normal file
1453
carrier/GPS_PPS.kicad_sch
Normal file
File diff suppressed because it is too large
Load Diff
2306
carrier/M2_Ekey.kicad_sch
Normal file
2306
carrier/M2_Ekey.kicad_sch
Normal file
File diff suppressed because it is too large
Load Diff
3548
carrier/Power.kicad_sch
Normal file
3548
carrier/Power.kicad_sch
Normal file
File diff suppressed because it is too large
Load Diff
4
carrier/fp-lib-table
Normal file
4
carrier/fp-lib-table
Normal file
@@ -0,0 +1,4 @@
|
||||
(fp_lib_table
|
||||
(version 7)
|
||||
(lib (name "CM5IO")(type "KiCad")(uri "${KIPRJMOD}/CM5IO.pretty")(options "")(descr ""))
|
||||
)
|
||||
4
carrier/sym-lib-table
Normal file
4
carrier/sym-lib-table
Normal file
@@ -0,0 +1,4 @@
|
||||
(sym_lib_table
|
||||
(version 7)
|
||||
(lib (name "CM5IO")(type "KiCad")(uri "${KIPRJMOD}/CM5IO.kicad_sym")(options "")(descr ""))
|
||||
)
|
||||
1807
datasheets/extracted/SC1466_43a9ec.json
Normal file
1807
datasheets/extracted/SC1466_43a9ec.json
Normal file
File diff suppressed because it is too large
Load Diff
17
datasheets/extracted/manifest.json
Normal file
17
datasheets/extracted/manifest.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"version": 2,
|
||||
"last_updated": "2026-06-24T12:39:26-04:00",
|
||||
"extractions": {
|
||||
"SC1466_43a9ec": {
|
||||
"file": "SC1466_43a9ec.json",
|
||||
"mpn": "SC1466",
|
||||
"category": "module",
|
||||
"source_pdf": "cm5-datasheet.pdf",
|
||||
"source_pdf_hash": "sha256:80070fefd8db6e8abc6e146c8b7b5fb318ba129cc1e28826936d547fde79c863",
|
||||
"extraction_date": "2026-06-24T16:39:26.264105+00:00",
|
||||
"extraction_score": 8.7,
|
||||
"extraction_version": 2,
|
||||
"pin_count": 200
|
||||
}
|
||||
}
|
||||
}
|
||||
Submodule refs/.history updated: ab8c8d6b26...a29df2ece6
247
tools/build_ekey.py
Normal file
247
tools/build_ekey.py
Normal file
@@ -0,0 +1,247 @@
|
||||
#!/usr/bin/env python3
|
||||
"""build_ekey.py — generate the M.2 E-key (Wi-Fi) sub-sheet for the CM5 carrier.
|
||||
|
||||
The inverse of retire_block.py: places a library connector + passives, attaches a
|
||||
named label at every USED pin connection point, and a (no_connect) at every unused
|
||||
pin. Connectivity is by KiCad's by-name net model (a label coincident with a pin
|
||||
endpoint joins that pin to the net) — no wire routing, which keeps generation exact.
|
||||
|
||||
Locked net map (see CM5_Carrier_Pinout_BOM.md §4, design confirmed E-key-only):
|
||||
PCIe link-0 -> the 9 freed CM5 pins ; +3V3_RF (4 pins) ; GND (11) ; W_DISABLE x2 pull-ups.
|
||||
RX is NOT AC-coupled on the carrier (caps live on the card's TX, per M.2 spec).
|
||||
|
||||
Emits carrier/M2_Ekey.kicad_sch. Pin connection point: abs=(px+lx, py-ly) for a
|
||||
rot0/no-mirror placement (the transform validated in retire_block.py).
|
||||
"""
|
||||
import re, sys, os, uuid as uuidlib
|
||||
|
||||
ROOT_UUID = "e63e39d7-6ac0-4ffd-8aa3-1841a4541b55"
|
||||
PROJECT = "CM5IO"
|
||||
SHEET_SYMBOL_UUID = "00000000-0000-0000-0000-00005 e000ekey".replace(" ", "") # placeholder, overwritten below
|
||||
KSS = "/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols"
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
CARRIER = os.path.join(HERE, "..", "carrier")
|
||||
|
||||
def U(label):
|
||||
return str(uuidlib.uuid5(uuidlib.NAMESPACE_DNS, "cm5carrier-ekey-" + label))
|
||||
|
||||
# deterministic sheet-symbol uuid (used in instance paths + the root sheet block)
|
||||
SHEET_SYMBOL_UUID = U("sheet-symbol")
|
||||
SHEET_FILE_UUID = U("sheet-file")
|
||||
|
||||
def paren_end(t, s):
|
||||
d = 0
|
||||
for i in range(s, len(t)):
|
||||
if t[i] == '(': d += 1
|
||||
elif t[i] == ')':
|
||||
d -= 1
|
||||
if d == 0: return i + 1
|
||||
raise ValueError("unbalanced")
|
||||
|
||||
def get_symbol(text, name):
|
||||
m = re.search(r'\(symbol\s+"' + re.escape(name) + r'"', text)
|
||||
if not m: return None
|
||||
s = m.start(); return text[s:paren_end(text, s)]
|
||||
|
||||
def lib_pins(seg):
|
||||
"""[(number, lx, ly, angle)] for a lib_symbol block (descends into _x_1 units)."""
|
||||
out = []
|
||||
for pm in re.finditer(r'\(pin\b', seg):
|
||||
s = pm.start(); ps = seg[s:paren_end(seg, s)]
|
||||
at = re.search(r'\(at\s+([\d.-]+)\s+([\d.-]+)\s+([\d.-]+)\)', ps)
|
||||
nu = re.search(r'\(number\s+"([^"]+)"', ps)
|
||||
if at and nu:
|
||||
out.append((nu.group(1), float(at.group(1)), float(at.group(2)), float(at.group(3))))
|
||||
return out
|
||||
|
||||
# ---- gather lib_symbol definitions -------------------------------------------------
|
||||
carrier_hs = open(os.path.join(CARRIER, "CM5_HighSpeed.kicad_sch")).read()
|
||||
conn_lib = open(os.path.join(KSS, "Connector.kicad_sym")).read()
|
||||
power_lib = open(os.path.join(KSS, "power.kicad_sym")).read()
|
||||
|
||||
# reuse R / C / GND verbatim from the carrier (guaranteed-compatible embeds)
|
||||
DEV_R = get_symbol(carrier_hs, "Device:R")
|
||||
DEV_C = get_symbol(carrier_hs, "Device:C")
|
||||
PWR_GND = get_symbol(carrier_hs, "power:GND")
|
||||
# socket + PWR_FLAG from KiCad libs, renamed to their full lib_id
|
||||
sock = get_symbol(conn_lib, "Bus_M.2_Socket_E")
|
||||
sock = sock.replace('(symbol "Bus_M.2_Socket_E"', '(symbol "Connector:Bus_M.2_Socket_E"', 1)
|
||||
|
||||
def set_pin_type(seg, number, newtype):
|
||||
"""Rewrite the electrical type of the lib pin whose (number "N") matches.
|
||||
The socket is typed from the host's view; for ERC across the connector we model
|
||||
the lines CM5 drives as 'passive' and the RX pair (card drives -> CM5) as 'output'."""
|
||||
for pm in re.finditer(r'\(pin\s+(\w+)\s+(\w+)', seg):
|
||||
s = pm.start(); blk = seg[s:paren_end(seg, s)]
|
||||
if re.search(r'\(number\s+"' + re.escape(number) + r'"', blk):
|
||||
head = re.match(r'\(pin\s+\w+', blk).group(0)
|
||||
return seg[:s] + blk.replace(head, f'(pin {newtype}', 1) + seg[s + len(blk):]
|
||||
return seg
|
||||
# Lines CM5 drives (TX 35/37, REFCLK 47/49, PERST 52): socket side passive.
|
||||
for n in ['35', '37', '47', '49', '52']:
|
||||
sock = set_pin_type(sock, n, 'passive')
|
||||
# Lines the CARD drives into CM5 inputs -> model socket side as output:
|
||||
# RX pair (41/43) + CLKREQ# (53) + WAKE# (55) are all CM5 inputs (open-drain for
|
||||
# CLKREQ/WAKE; the card pulls them). 'output' gives each net a driver for ERC.
|
||||
for n in ['41', '43', '53', '55']:
|
||||
sock = set_pin_type(sock, n, 'output')
|
||||
pflag = get_symbol(power_lib, "PWR_FLAG")
|
||||
pflag = pflag.replace('(symbol "PWR_FLAG"', '(symbol "power:PWR_FLAG"', 1)
|
||||
for nm, blk in [("Device:R", DEV_R), ("Device:C", DEV_C), ("power:GND", PWR_GND),
|
||||
("Connector:Bus_M.2_Socket_E", sock), ("power:PWR_FLAG", pflag)]:
|
||||
if not blk: sys.exit("missing lib_symbol: " + nm)
|
||||
|
||||
SOCK_PINS = {n: (lx, ly, a) for n, lx, ly, a in lib_pins(sock)} # 67 pins
|
||||
|
||||
# ---- the net map -------------------------------------------------------------------
|
||||
# pin -> ('hier', net) cross-sheet PCIe ; ('gnd',) ; ('v33',) ; ('loc', net) local
|
||||
HIER = {
|
||||
'35': 'PCIE_TX_P', '37': 'PCIE_TX_N', '41': 'PCIE_RX_P', '43': 'PCIE_RX_N',
|
||||
'47': 'PCIE_CLK_P', '49': 'PCIE_CLK_N', '52': 'PCIE_nRST', '53': 'PCIE_nCLKREQ',
|
||||
'55': 'PCIE_nWAKE',
|
||||
}
|
||||
V33 = ['2', '4', '72', '74']
|
||||
GND = ['1', '7', '18', '33', '39', '45', '51', '57', '63', '69', '75']
|
||||
LOCAL = {'56': 'WIFI_DISABLE#', '54': 'WIFI_DISABLE2#'}
|
||||
used = set(HIER) | set(V33) | set(GND) | set(LOCAL)
|
||||
NC = sorted((p for p in SOCK_PINS if p not in used), key=lambda x: int(x))
|
||||
|
||||
assert len(used) + len(NC) == len(SOCK_PINS), (len(used), len(NC), len(SOCK_PINS))
|
||||
|
||||
# ---- placement ---------------------------------------------------------------------
|
||||
def g(v): # snap to the 1.27mm (50mil) schematic grid so labels bind to pins
|
||||
return round(round(v / 1.27) * 1.27, 2)
|
||||
PX, PY = g(170.0), g(105.0)
|
||||
def pin_abs(num):
|
||||
lx, ly, a = SOCK_PINS[num]
|
||||
return (round(PX + lx, 2), round(PY - ly, 2), a)
|
||||
|
||||
def lbl_angle(num):
|
||||
lx, ly, a = SOCK_PINS[num]
|
||||
if abs(lx) >= abs(ly): return 0 if lx > 0 else 180
|
||||
return 90 if ly > 0 else 270
|
||||
|
||||
def hier_label(name, x, y, ang, shape="bidirectional"):
|
||||
return (f'\t(hierarchical_label "{name}"\n\t\t(shape {shape})\n\t\t(at {x} {y} {ang})\n'
|
||||
f'\t\t(effects (font (size 1.27 1.27)) (justify left))\n\t\t(uuid "{U("hl-"+name+"-"+str(x)+"-"+str(y))}")\n\t)\n')
|
||||
|
||||
def glob_label(name, x, y, ang):
|
||||
return (f'\t(global_label "{name}"\n\t\t(shape input)\n\t\t(at {x} {y} {ang})\n'
|
||||
f'\t\t(effects (font (size 1.27 1.27)) (justify left))\n\t\t(uuid "{U("gl-"+name+"-"+str(x)+"-"+str(y))}")\n\t)\n')
|
||||
|
||||
def loc_label(name, x, y, ang):
|
||||
return (f'\t(label "{name}"\n\t\t(at {x} {y} {ang})\n'
|
||||
f'\t\t(effects (font (size 1.27 1.27)) (justify left))\n\t\t(uuid "{U("ll-"+name+"-"+str(x)+"-"+str(y))}")\n\t)\n')
|
||||
|
||||
def no_connect(x, y, tag):
|
||||
tag = tag if tag[-1].isdigit() else tag + "1" # refs must end in a digit (KiCad annotation)
|
||||
return f'\t(no_connect (at {x} {y}) (uuid "{U("nc-"+tag)}"))\n'
|
||||
|
||||
def gnd_sym(x, y, tag):
|
||||
tag = tag if tag[-1].isdigit() else tag + "1" # refs must end in a digit (KiCad annotation)
|
||||
return (f'\t(symbol\n\t\t(lib_id "power:GND")\n\t\t(at {x} {y} 0)\n\t\t(unit 1)\n'
|
||||
f'\t\t(exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no)\n'
|
||||
f'\t\t(uuid "{U("gnd-"+tag)}")\n'
|
||||
f'\t\t(property "Reference" "#PWR{tag}" (at {x} {y+3.0} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
f'\t\t(property "Value" "GND" (at {x} {y+2.0} 0) (effects (font (size 1.27 1.27))))\n'
|
||||
f'\t\t(property "Footprint" "" (at {x} {y} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
f'\t\t(property "Datasheet" "" (at {x} {y} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
f'\t\t(pin "1" (uuid "{U("gndpin-"+tag)}"))\n'
|
||||
f'\t\t(instances (project "{PROJECT}" (path "/{ROOT_UUID}/{SHEET_SYMBOL_UUID}" (reference "#PWR{tag}") (unit 1))))\n\t)\n')
|
||||
|
||||
def pwr_flag(x, y, tag):
|
||||
tag = tag if tag[-1].isdigit() else tag + "1" # refs must end in a digit (KiCad annotation)
|
||||
return (f'\t(symbol\n\t\t(lib_id "power:PWR_FLAG")\n\t\t(at {x} {y} 0)\n\t\t(unit 1)\n'
|
||||
f'\t\t(exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no)\n'
|
||||
f'\t\t(uuid "{U("pflag-"+tag)}")\n'
|
||||
f'\t\t(property "Reference" "#FLG{tag}" (at {x} {y-3.0} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
f'\t\t(property "Value" "PWR_FLAG" (at {x} {y-2.0} 0) (effects (font (size 1.27 1.27))))\n'
|
||||
f'\t\t(property "Footprint" "" (at {x} {y} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
f'\t\t(property "Datasheet" "" (at {x} {y} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
f'\t\t(pin "1" (uuid "{U("pflagpin-"+tag)}"))\n'
|
||||
f'\t\t(instances (project "{PROJECT}" (path "/{ROOT_UUID}/{SHEET_SYMBOL_UUID}" (reference "#FLG{tag}") (unit 1))))\n\t)\n')
|
||||
|
||||
def passive(lib_id, ref, value, footprint, x, y, tag):
|
||||
return (f'\t(symbol\n\t\t(lib_id "{lib_id}")\n\t\t(at {x} {y} 0)\n\t\t(unit 1)\n'
|
||||
f'\t\t(exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no)\n'
|
||||
f'\t\t(uuid "{U("sym-"+ref)}")\n'
|
||||
f'\t\t(property "Reference" "{ref}" (at {x+2.54} {y-1.0} 0) (effects (font (size 1.27 1.27)) (justify left)))\n'
|
||||
f'\t\t(property "Value" "{value}" (at {x+2.54} {y+1.0} 0) (effects (font (size 1.27 1.27)) (justify left)))\n'
|
||||
f'\t\t(property "Footprint" "{footprint}" (at {x} {y} 90) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
f'\t\t(property "Datasheet" "" (at {x} {y} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
f'\t\t(pin "1" (uuid "{U("p1-"+ref)}"))\n'
|
||||
f'\t\t(pin "2" (uuid "{U("p2-"+ref)}"))\n'
|
||||
f'\t\t(instances (project "{PROJECT}" (path "/{ROOT_UUID}/{SHEET_SYMBOL_UUID}" (reference "{ref}") (unit 1))))\n\t)\n')
|
||||
|
||||
def socket_symbol():
|
||||
# the placed J_E (multi-unit symbol uses unit 1; both graphic units share lib def)
|
||||
props = ('\t\t(property "Reference" "J_E1" (at 170 53 0) (effects (font (size 1.27 1.27))))\n'
|
||||
'\t\t(property "Value" "M.2_Ekey_WiFi" (at 170 55 0) (effects (font (size 1.27 1.27))))\n'
|
||||
'\t\t(property "Footprint" "Connector_PCBEdge:M.2_2230-xx-E" (at 170 105 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
'\t\t(property "Datasheet" "" (at 170 105 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
'\t\t(property "Description" "M.2 A+E key socket, Wi-Fi (AsiaRF AW7915/MT7915)" (at 170 105 0) (effects (font (size 1.27 1.27)) (hide yes)))\n')
|
||||
pins = "".join(f'\t\t(pin "{n}" (uuid "{U("jepin-"+n)}"))\n' for n in sorted(SOCK_PINS, key=int))
|
||||
return (f'\t(symbol\n\t\t(lib_id "Connector:Bus_M.2_Socket_E")\n\t\t(at {PX} {PY} 0)\n\t\t(unit 1)\n'
|
||||
f'\t\t(exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no)\n'
|
||||
f'\t\t(uuid "{U("J_E1")}")\n' + props + pins +
|
||||
f'\t\t(instances (project "{PROJECT}" (path "/{ROOT_UUID}/{SHEET_SYMBOL_UUID}" (reference "J_E1") (unit 1))))\n\t)\n')
|
||||
|
||||
# ---- assemble the sheet ------------------------------------------------------------
|
||||
body = []
|
||||
body.append(socket_symbol())
|
||||
|
||||
# connector pin labels / NC / GND
|
||||
for num, net in HIER.items():
|
||||
# global labels: connect to the matching CM5-side labels across sheets without
|
||||
# parent sheet-pin plumbing (the 9 freed CM5 PCIe pins get the same global names).
|
||||
x, y, _ = pin_abs(num); body.append(glob_label(net, x, y, lbl_angle(num)))
|
||||
for num in V33:
|
||||
x, y, _ = pin_abs(num); body.append(glob_label("+3V3_RF", x, y, lbl_angle(num)))
|
||||
for num, net in LOCAL.items():
|
||||
x, y, _ = pin_abs(num); body.append(loc_label(net, x, y, lbl_angle(num)))
|
||||
for num in GND:
|
||||
x, y, _ = pin_abs(num); body.append(gnd_sym(x, y, num))
|
||||
for num in NC:
|
||||
x, y, _ = pin_abs(num); body.append(no_connect(x, y, num))
|
||||
|
||||
# pull-up resistors: pin1(top) -> signal label ; pin2(bottom) -> +3V3_RF
|
||||
def place_rc(lib_id, ref, value, fp, x, y, top_label_fn, top_name, bot_name):
|
||||
body.append(passive(lib_id, ref, value, fp, x, y, ref))
|
||||
body.append(top_label_fn(top_name, x, round(y - 3.81, 2), 90)) # pin1
|
||||
body.append(glob_label(bot_name, x, round(y + 3.81, 2), 270)) # pin2
|
||||
|
||||
place_rc("Device:R", "R_E1", "10k", "Resistor_SMD:R_0402_1005Metric", g(215.0), g(80.0), loc_label, "WIFI_DISABLE#", "+3V3_RF")
|
||||
place_rc("Device:R", "R_E2", "10k", "Resistor_SMD:R_0402_1005Metric", g(225.0), g(80.0), loc_label, "WIFI_DISABLE2#", "+3V3_RF")
|
||||
|
||||
# decoupling caps: pin1 -> +3V3_RF ; pin2 -> GND
|
||||
for i, (ref, val) in enumerate([("C_E1", "10u"), ("C_E2", "100n"), ("C_E3", "100n")]):
|
||||
cx = g(215.0 + i * 7.62); cy = g(110.0)
|
||||
body.append(passive("Device:C", ref, val, "Capacitor_SMD:C_0402_1005Metric", cx, cy, ref))
|
||||
body.append(glob_label("+3V3_RF", cx, round(cy - 3.81, 2), 90)) # pin1
|
||||
body.append(gnd_sym(cx, round(cy + 3.81, 2), ref + "g")) # pin2 -> GND symbol
|
||||
|
||||
# +3V3_RF is driven by the Power sheet's U_BUCK33R (PWR_FLAG lives there).
|
||||
|
||||
# ---- file scaffold -----------------------------------------------------------------
|
||||
libsyms = "\n".join([DEV_R, DEV_C, PWR_GND, pflag, sock])
|
||||
sheet = (
|
||||
'(kicad_sch\n'
|
||||
'\t(version 20231120)\n\t(generator "eeschema")\n\t(generator_version "8.0")\n'
|
||||
f'\t(uuid "{SHEET_FILE_UUID}")\n\t(paper "A4")\n'
|
||||
'\t(title_block\n\t\t(title "CM5 Carrier - M.2 E-key (Wi-Fi)")\n\t\t(rev "A")\n'
|
||||
'\t\t(comment 1 "AW7915 / MT7915, PCIe x1 Gen2, USB NC")\n\t)\n'
|
||||
'\t(lib_symbols\n' + libsyms + '\n\t)\n'
|
||||
+ "".join(body) +
|
||||
'\t(sheet_instances\n\t\t(path "/"\n\t\t\t(page "4")\n\t\t)\n\t)\n'
|
||||
')\n'
|
||||
)
|
||||
|
||||
assert sheet.count('(') == sheet.count(')'), f"UNBALANCED ({sheet.count('(')} vs {sheet.count(')')})"
|
||||
out = os.path.join(CARRIER, "M2_Ekey.kicad_sch")
|
||||
print(f"pins: {len(SOCK_PINS)} used: {len(used)} (hier {len(HIER)}, v33 {len(V33)}, gnd {len(GND)}, local {len(LOCAL)}) NC: {len(NC)}")
|
||||
print(f"sheet-symbol uuid: {SHEET_SYMBOL_UUID}")
|
||||
print(f"parens balanced: {sheet.count('(')}")
|
||||
if '--apply' in sys.argv:
|
||||
open(out, 'w').write(sheet); print("WROTE " + out)
|
||||
else:
|
||||
print("DRY RUN (pass --apply to write " + out + ")")
|
||||
168
tools/build_gps.py
Normal file
168
tools/build_gps.py
Normal file
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
"""build_gps.py — generate the CM5-carrier GPS + PPS distribution sheet.
|
||||
|
||||
Same connectivity model as build_power.py: every pin gets a coincident named
|
||||
label / GND symbol / no_connect; nets form by name (grid-snapped 1.27mm).
|
||||
|
||||
Frozen spec (CM5_Carrier_Pinout_BOM.md §6, CM5_Carrier_Design.md §5a):
|
||||
J_GPS(5-pin): 1=PPS 2=NMEA_RX(GPS TX -> CM5 GPIO5/UART2 RX)
|
||||
3=NMEA_TX(CM5 GPIO4/UART2 TX -> GPS RX) 4=+3V3_AUX 5=GND
|
||||
GPS_PPS_RAW -> TVS -> 74LVC1G17 Schmitt (3V3) -> PPS_CLEAN
|
||||
-> Rs 33R -> SYNC_OUT (CM5 pin 18, Ethernet_SYNC_OUT, 3.3V bidi, cfg input)
|
||||
-> J_PPS1 (UWB tap) / J_PPS2 (scope tap), 2-pin each
|
||||
Board is always a PPS SINK (GPS-sourced).
|
||||
|
||||
Cross-sheet nets are GLOBAL: +3V3_AUX (Power sheet flag drives it), and
|
||||
GPIO4/GPIO5/SYNC_OUT (their CM5_GPIO local labels were converted to global —
|
||||
net names unchanged; nothing else in the design used those names).
|
||||
J_GPS pins 1/2 are re-typed 'output' (the GPS module drives PPS and TX into
|
||||
the board) so the analyzer's no_driver gate sees real drivers (E-key lesson).
|
||||
"""
|
||||
import re, sys, os, uuid as uuidlib
|
||||
|
||||
ROOT_UUID = "e63e39d7-6ac0-4ffd-8aa3-1841a4541b55"
|
||||
PROJECT = "CM5IO"
|
||||
KSS = "/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols"
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
CARRIER = os.path.join(HERE, "..", "carrier")
|
||||
|
||||
def U(label): return str(uuidlib.uuid5(uuidlib.NAMESPACE_DNS, "cm5carrier-gps-" + label))
|
||||
SHEET_SYMBOL_UUID = U("sheet-symbol")
|
||||
SHEET_FILE_UUID = U("sheet-file")
|
||||
|
||||
def g(v): return round(round(v / 1.27) * 1.27, 2)
|
||||
|
||||
def paren_end(t, s):
|
||||
d = 0
|
||||
for i in range(s, len(t)):
|
||||
if t[i] == '(': d += 1
|
||||
elif t[i] == ')':
|
||||
d -= 1
|
||||
if d == 0: return i + 1
|
||||
raise ValueError("unbalanced")
|
||||
|
||||
def get_symbol(text, name):
|
||||
m = re.search(r'\(symbol\s+"' + re.escape(name) + r'"', text)
|
||||
return text[m.start():paren_end(text, m.start())] if m else None
|
||||
|
||||
def lib_pins(seg):
|
||||
out = {}
|
||||
for pm in re.finditer(r'\(pin\b', seg):
|
||||
s = pm.start(); ps = seg[s:paren_end(seg, s)]
|
||||
at = re.search(r'\(at\s+([\d.-]+)\s+([\d.-]+)\s+([\d.-]+)\)', ps)
|
||||
nu = re.search(r'\(number\s+"([^"]+)"', ps)
|
||||
if at and nu:
|
||||
out.setdefault(nu.group(1), (float(at.group(1)), float(at.group(2)), float(at.group(3))))
|
||||
return out
|
||||
|
||||
def set_pin_type(seg, number, newtype):
|
||||
for pm in re.finditer(r'\(pin\s+(\w+)\s+(\w+)', seg):
|
||||
s = pm.start(); blk = seg[s:paren_end(seg, s)]
|
||||
if re.search(r'\(number\s+"' + re.escape(number) + r'"', blk):
|
||||
head = re.match(r'\(pin\s+\w+', blk).group(0)
|
||||
return seg[:s] + blk.replace(head, f'(pin {newtype}', 1) + seg[s + len(blk):]
|
||||
return seg
|
||||
|
||||
# ---- lib_symbol sources -----------------------------------------------------------
|
||||
carrier_hs = open(os.path.join(CARRIER, "CM5_HighSpeed.kicad_sch")).read()
|
||||
LIBS = {}
|
||||
def add_from(lib_id, srctext, srcname=None, retype=None):
|
||||
srcname = srcname or lib_id.split(':')[-1]
|
||||
blk = get_symbol(srctext, lib_id) or get_symbol(srctext, srcname)
|
||||
if not blk: sys.exit("missing lib_symbol " + lib_id)
|
||||
if ('"' + lib_id + '"') not in blk[:60]:
|
||||
blk = blk.replace('(symbol "' + srcname + '"', '(symbol "' + lib_id + '"', 1)
|
||||
for num, ty in (retype or {}).items():
|
||||
blk = set_pin_type(blk, num, ty)
|
||||
LIBS[lib_id] = (blk, lib_pins(blk))
|
||||
|
||||
for lid in ["Device:R", "Device:C", "power:GND"]:
|
||||
add_from(lid, carrier_hs)
|
||||
add_from("Device:D_TVS", open(f"{KSS}/Device.kicad_sym").read(), "D_TVS")
|
||||
# GPS module DRIVES pins 1 (PPS) and 2 (its TX) -> 'output' for driver-aware checks
|
||||
add_from("Connector_Generic:Conn_01x05", open(f"{KSS}/Connector_Generic.kicad_sym").read(),
|
||||
"Conn_01x05", retype={'1': 'output', '2': 'output'})
|
||||
add_from("Connector_Generic:Conn_01x02", open(f"{KSS}/Connector_Generic.kicad_sym").read(), "Conn_01x02")
|
||||
add_from("74xGxx:74LVC1G17", open(f"{KSS}/74xGxx.kicad_sym").read(), "74LVC1G17")
|
||||
|
||||
GLOBAL_NETS = {"+3V3_AUX", "GPIO4", "GPIO5", "SYNC_OUT"}
|
||||
HIER_NETS = set()
|
||||
|
||||
# ---- placement engine (same as build_power.py, + ('nc',) pins) --------------------
|
||||
body = []
|
||||
def emit_label(net, x, y, ang=0):
|
||||
body.append(f'\t(label "{net}"\n\t\t(at {x} {y} {ang})\n'
|
||||
f'\t\t(effects (font (size 1.27 1.27)) (justify left))\n\t\t(uuid "{U("l-"+net+f"-{x}-{y}")}")\n\t)\n')
|
||||
def emit_glabel(net, x, y, ang=0):
|
||||
body.append(f'\t(global_label "{net}"\n\t\t(shape input)\n\t\t(at {x} {y} {ang})\n'
|
||||
f'\t\t(effects (font (size 1.27 1.27)) (justify left))\n\t\t(uuid "{U("gl-"+net+f"-{x}-{y}")}")\n\t)\n')
|
||||
def emit_nc(x, y, tag):
|
||||
tag = tag if tag[-1].isdigit() else tag + "1" # refs must end in a digit (KiCad annotation)
|
||||
body.append(f'\t(no_connect (at {x} {y}) (uuid "{U("nc-"+tag)}"))\n')
|
||||
def emit_gnd(x, y, tag):
|
||||
tag = tag if tag[-1].isdigit() else tag + "1" # refs must end in a digit (KiCad annotation)
|
||||
body.append(f'\t(symbol\n\t\t(lib_id "power:GND")\n\t\t(at {x} {y} 0)\n\t\t(unit 1)\n'
|
||||
f'\t\t(exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no)\n\t\t(uuid "{U("gnd-"+tag)}")\n'
|
||||
f'\t\t(property "Reference" "#PWR{tag}" (at {x} {y+3} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
f'\t\t(property "Value" "GND" (at {x} {y+2} 0) (effects (font (size 1.27 1.27))))\n'
|
||||
f'\t\t(property "Footprint" "" (at {x} {y} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
f'\t\t(property "Datasheet" "" (at {x} {y} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
f'\t\t(pin "1" (uuid "{U("gp-"+tag)}"))\n'
|
||||
f'\t\t(instances (project "{PROJECT}" (path "/{ROOT_UUID}/{SHEET_SYMBOL_UUID}" (reference "#PWR{tag}") (unit 1))))\n\t)\n')
|
||||
|
||||
def place(lib_id, ref, value, footprint, ox, oy, pin_nets):
|
||||
ox, oy = g(ox), g(oy)
|
||||
_, pins = LIBS[lib_id]
|
||||
extra = ('\t\t(property "Description" "" (at %g %g 0) (effects (font (size 1.27 1.27)) (hide yes)))\n' % (ox, oy))
|
||||
sym = (f'\t(symbol\n\t\t(lib_id "{lib_id}")\n\t\t(at {ox} {oy} 0)\n\t\t(unit 1)\n'
|
||||
f'\t\t(exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no)\n\t\t(uuid "{U("sym-"+ref)}")\n'
|
||||
f'\t\t(property "Reference" "{ref}" (at {ox+2.54} {oy-1} 0) (effects (font (size 1.27 1.27)) (justify left)))\n'
|
||||
f'\t\t(property "Value" "{value}" (at {ox+2.54} {oy+1} 0) (effects (font (size 1.27 1.27)) (justify left)))\n'
|
||||
f'\t\t(property "Footprint" "{footprint}" (at {ox} {oy} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
f'\t\t(property "Datasheet" "" (at {ox} {oy} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n' + extra)
|
||||
for num in sorted(pins, key=lambda n: int(re.sub(r'\D', '', n) or 0)):
|
||||
sym += f'\t\t(pin "{num}" (uuid "{U("p-"+ref+"-"+num)}"))\n'
|
||||
sym += f'\t\t(instances (project "{PROJECT}" (path "/{ROOT_UUID}/{SHEET_SYMBOL_UUID}" (reference "{ref}") (unit 1))))\n\t)\n'
|
||||
body.append(sym)
|
||||
for num, net in pin_nets.items():
|
||||
lx, ly, _ = pins[num]; px, py = round(ox + lx, 2), round(oy - ly, 2)
|
||||
if net == ('gnd',): emit_gnd(px, py, ref + num)
|
||||
elif net == ('nc',): emit_nc(px, py, ref + num)
|
||||
elif isinstance(net, tuple) and net[0] == 'global': emit_glabel(net[1], px, py)
|
||||
elif net in GLOBAL_NETS: emit_glabel(net, px, py)
|
||||
else: emit_label(net, px, py)
|
||||
return ox, oy
|
||||
|
||||
GND = ('gnd',); NC = ('nc',)
|
||||
|
||||
# ---- the GPS + PPS block ----------------------------------------------------------
|
||||
place("Connector_Generic:Conn_01x05", "J_GPS1", "GPS_5pin_PPS_UART", "Connector_PinHeader_2.54mm:PinHeader_1x05_P2.54mm_Vertical",
|
||||
60, 80, {'1': 'GPS_PPS_RAW', '2': 'GPIO5', '3': 'GPIO4', '4': '+3V3_AUX', '5': GND})
|
||||
place("Device:D_TVS", "TVS_PPS1", "PESD3V3L1BA", "Diode_SMD:D_SOD-323", 85, 95, {'1': 'GPS_PPS_RAW', '2': GND})
|
||||
# 74LVC1G17: 1=NC 2=A 3=GND 4=Y 5=VCC (KiCad lib types: input/power_in/output/power_in)
|
||||
place("74xGxx:74LVC1G17", "U_PPS1", "74LVC1G17", "Package_TO_SOT_SMD:SOT-23-5",
|
||||
110, 80, {'2': 'GPS_PPS_RAW', '4': 'PPS_CLEAN', '5': '+3V3_AUX', '3': GND, '1': NC})
|
||||
place("Device:C", "C_PPS1", "100n", "Capacitor_SMD:C_0402_1005Metric", 100, 95, {'1': '+3V3_AUX', '2': GND})
|
||||
place("Device:C", "C_GPS1", "10u", "Capacitor_SMD:C_0603_1608Metric", 70, 95, {'1': '+3V3_AUX', '2': GND})
|
||||
# series R guards the LVC1G17 output against a driver fight (CM5 pin 18 is bidi;
|
||||
# board policy configures it as input — spec Rs 33-100R, pick 33R)
|
||||
place("Device:R", "R_PPS1", "33", "Resistor_SMD:R_0402_1005Metric", 140, 80, {'1': 'PPS_CLEAN', '2': 'SYNC_OUT'})
|
||||
place("Connector_Generic:Conn_01x02", "J_PPS1", "PPS_tap_UWB", "Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical",
|
||||
170, 75, {'1': 'PPS_CLEAN', '2': GND})
|
||||
place("Connector_Generic:Conn_01x02", "J_PPS2", "PPS_tap_scope", "Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical",
|
||||
170, 90, {'1': 'PPS_CLEAN', '2': GND})
|
||||
|
||||
# ---- file scaffold ----------------------------------------------------------------
|
||||
libsyms = "\n".join(blk for blk, _ in LIBS.values())
|
||||
sheet = ('(kicad_sch\n\t(version 20231120)\n\t(generator "eeschema")\n\t(generator_version "8.0")\n'
|
||||
f'\t(uuid "{SHEET_FILE_UUID}")\n\t(paper "A4")\n'
|
||||
'\t(title_block\n\t\t(title "CM5 Carrier - GPS + PPS distribution")\n\t\t(rev "A")\n'
|
||||
'\t\t(comment 1 "GPS conn (PPS+UART2+3V3_AUX) -> TVS -> LVC1G17 -> CM5 SYNC + 2 taps; PPS sink only")\n\t)\n'
|
||||
'\t(lib_symbols\n' + libsyms + '\n\t)\n' + "".join(body) +
|
||||
'\t(sheet_instances\n\t\t(path "/"\n\t\t\t(page "6")\n\t\t)\n\t)\n)\n')
|
||||
assert sheet.count('(') == sheet.count(')'), f"UNBALANCED {sheet.count('(')} vs {sheet.count(')')}"
|
||||
print("sheet-symbol uuid:", SHEET_SYMBOL_UUID)
|
||||
print("parens balanced:", sheet.count('('))
|
||||
out = os.path.join(CARRIER, "GPS_PPS.kicad_sch")
|
||||
if '--apply' in sys.argv: open(out, 'w').write(sheet); print("WROTE " + out)
|
||||
else: print("DRY RUN (--apply to write)")
|
||||
304
tools/build_power.py
Normal file
304
tools/build_power.py
Normal file
@@ -0,0 +1,304 @@
|
||||
#!/usr/bin/env python3
|
||||
"""build_power.py — generate the CM5-carrier Power sheet, stage by stage.
|
||||
|
||||
Same connectivity model as build_ekey.py: every component pin gets a named label
|
||||
(or a power:GND symbol) coincident with its connection point, so nets form by name
|
||||
with no wire routing. Coords are grid-snapped to 1.27mm so labels bind to pins.
|
||||
|
||||
STAGE 1 (this file): input protection —
|
||||
J_PWR(15V) -> D_REV(LM74700 ideal-diode + Q_REV N-FET) -> VIN_PROT, with input TVS
|
||||
+ bulk. Reverse-polarity + reverse-current via the ideal-diode controller.
|
||||
Later stages (bucks) append to PLACEMENTS and re-run.
|
||||
|
||||
Datasheet items to CONFIRM against LM74700-Q1 (values, not topology):
|
||||
- C_VCAP value/reference (charge-pump reservoir; ~1uF typ)
|
||||
- EN handling (tied to +15V_IN for always-on; EN abs-max covers 15V)
|
||||
- Q_REV FET pick (Vds>=30V, low Rds(on), SOA for inrush) + optional gate resistor
|
||||
- TVS standoff (SMBJ18A: 18V standoff / ~29V clamp < buck Vin rating)
|
||||
"""
|
||||
import re, sys, os, uuid as uuidlib
|
||||
|
||||
ROOT_UUID = "e63e39d7-6ac0-4ffd-8aa3-1841a4541b55"
|
||||
PROJECT = "CM5IO"
|
||||
KSS = "/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols"
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
CARRIER = os.path.join(HERE, "..", "carrier")
|
||||
|
||||
def U(label): return str(uuidlib.uuid5(uuidlib.NAMESPACE_DNS, "cm5carrier-power-" + label))
|
||||
SHEET_SYMBOL_UUID = U("sheet-symbol")
|
||||
SHEET_FILE_UUID = U("sheet-file")
|
||||
|
||||
def g(v): return round(round(v / 1.27) * 1.27, 2)
|
||||
|
||||
def paren_end(t, s):
|
||||
d = 0
|
||||
for i in range(s, len(t)):
|
||||
if t[i] == '(': d += 1
|
||||
elif t[i] == ')':
|
||||
d -= 1
|
||||
if d == 0: return i + 1
|
||||
raise ValueError("unbalanced")
|
||||
|
||||
def get_symbol(text, name):
|
||||
m = re.search(r'\(symbol\s+"' + re.escape(name) + r'"', text)
|
||||
return text[m.start():paren_end(text, m.start())] if m else None
|
||||
|
||||
def lib_pins(seg):
|
||||
out = {}
|
||||
for pm in re.finditer(r'\(pin\b', seg):
|
||||
s = pm.start(); ps = seg[s:paren_end(seg, s)]
|
||||
at = re.search(r'\(at\s+([\d.-]+)\s+([\d.-]+)\s+([\d.-]+)\)', ps)
|
||||
nu = re.search(r'\(number\s+"([^"]+)"', ps)
|
||||
if at and nu:
|
||||
out.setdefault(nu.group(1), (float(at.group(1)), float(at.group(2)), float(at.group(3))))
|
||||
return out
|
||||
|
||||
# ---- lib_symbol sources -----------------------------------------------------------
|
||||
carrier_hs = open(os.path.join(CARRIER, "CM5_HighSpeed.kicad_sch")).read()
|
||||
LIBS = {} # lib_id -> (embed_block, {pin:(lx,ly,ang)})
|
||||
def add_from(lib_id, srctext, srcname=None):
|
||||
srcname = srcname or lib_id.split(':')[-1]
|
||||
blk = get_symbol(srctext, lib_id) or get_symbol(srctext, srcname)
|
||||
if not blk: sys.exit("missing lib_symbol " + lib_id)
|
||||
if ('"' + lib_id + '"') not in blk[:60]:
|
||||
blk = blk.replace('(symbol "' + srcname + '"', '(symbol "' + lib_id + '"', 1)
|
||||
LIBS[lib_id] = (blk, lib_pins(blk))
|
||||
|
||||
# reuse R/C/C_Polarized/GND verbatim from the carrier (guaranteed compatible)
|
||||
for lid in ["Device:R", "Device:C", "Device:C_Polarized", "power:GND"]:
|
||||
add_from(lid, carrier_hs)
|
||||
add_from("Device:L", open(f"{KSS}/Device.kicad_sym").read(), "L")
|
||||
|
||||
def register_lm61460():
|
||||
"""Hand-authored lib_symbol for LM61460-Q1 (14-pin HTSSOP-1EP + EP=15),
|
||||
built from the datasheet pin table (SNVSB70F). VREF(FB)=1.0V, VIN abs-max 42V.
|
||||
Pin types picked for cross-sheet ERC: VIN/PGND/AGND/BIAS/EP power_in, VCC power_out,
|
||||
SW output, FB/EN input, RT/CBOOT/RBOOT passive, PGOOD open_collector."""
|
||||
P = [('8','VIN1','power_in',-16.51,15.24,0), ('12','VIN2','power_in',-16.51,12.7,0),
|
||||
('7','EN/SYNC','input',-16.51,5.08,0), ('1','BIAS','power_in',-16.51,0,0),
|
||||
('6','RT','passive',-16.51,-7.62,0),
|
||||
('10','SW','output',16.51,15.24,180), ('14','CBOOT','passive',16.51,12.7,180),
|
||||
('13','RBOOT','passive',16.51,10.16,180), ('2','VCC','power_out',16.51,5.08,180),
|
||||
('4','FB','passive',16.51,-2.54,180), ('5','PGOOD','open_collector',16.51,-7.62,180),
|
||||
('9','PGND1','power_in',-7.62,-21.59,90), ('11','PGND2','power_in',-2.54,-21.59,90),
|
||||
('3','AGND','power_in',2.54,-21.59,90), ('15','EP','power_in',7.62,-21.59,90)]
|
||||
pt = ""
|
||||
for num, name, ty, lx, ly, ang in P:
|
||||
pt += (f'\t\t\t(pin {ty} line (at {lx} {ly} {ang}) (length 3.81)\n'
|
||||
f'\t\t\t\t(name "{name}" (effects (font (size 1.27 1.27))))\n'
|
||||
f'\t\t\t\t(number "{num}" (effects (font (size 1.27 1.27))))\n\t\t\t)\n')
|
||||
blk = ('\t(symbol "CM5IO:LM61460-Q1"\n\t\t(pin_names (offset 1.016))\n'
|
||||
'\t\t(exclude_from_sim no)\n\t\t(in_bom yes)\n\t\t(on_board yes)\n'
|
||||
'\t\t(property "Reference" "U" (at 0 22.86 0) (effects (font (size 1.27 1.27))))\n'
|
||||
'\t\t(property "Value" "LM61460-Q1" (at 0 -26.67 0) (effects (font (size 1.27 1.27))))\n'
|
||||
'\t\t(property "Footprint" "Package_SO:Texas_HTSSOP-14-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask3.155x3.255mm_ThermalVias" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
'\t\t(property "Datasheet" "https://www.ti.com/lit/ds/symlink/lm61460-q1.pdf" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
'\t\t(symbol "LM61460-Q1_0_1"\n'
|
||||
'\t\t\t(rectangle (start -12.7 20.32) (end 12.7 -20.32) (stroke (width 0.254) (type default)) (fill (type background)))\n\t\t)\n'
|
||||
f'\t\t(symbol "LM61460-Q1_1_1"\n{pt}\t\t)\n\t)')
|
||||
LIBS["CM5IO:LM61460-Q1"] = (blk, lib_pins(blk))
|
||||
register_lm61460()
|
||||
|
||||
def register_tps54202():
|
||||
"""Hand-authored lib_symbol for TPS54202 (SOT-23-6), from the datasheet pin table
|
||||
(SLVSD26C): GND=1, SW=2, VIN=3, FB=4, EN=5, BOOT=6. VFB=0.596V, VIN abs-max 30V,
|
||||
fixed 500kHz. The KiCad lib symbol extends TPS54302 (no own pins) so it can't be
|
||||
embedded standalone. FB/EN typed passive: both are high-Z sense pins on
|
||||
resistor-divider-only nets (else ERC/analyzer flag no_driver)."""
|
||||
P = [('3','VIN','power_in',-13.97,5.08,0), ('5','EN','passive',-13.97,0,0),
|
||||
('6','BOOT','passive',13.97,5.08,180), ('2','SW','output',13.97,2.54,180),
|
||||
('4','FB','passive',13.97,-2.54,180), ('1','GND','power_in',0,-13.97,90)]
|
||||
pt = ""
|
||||
for num, name, ty, lx, ly, ang in P:
|
||||
pt += (f'\t\t\t(pin {ty} line (at {lx} {ly} {ang}) (length 3.81)\n'
|
||||
f'\t\t\t\t(name "{name}" (effects (font (size 1.27 1.27))))\n'
|
||||
f'\t\t\t\t(number "{num}" (effects (font (size 1.27 1.27))))\n\t\t\t)\n')
|
||||
blk = ('\t(symbol "CM5IO:TPS54202"\n\t\t(pin_names (offset 1.016))\n'
|
||||
'\t\t(exclude_from_sim no)\n\t\t(in_bom yes)\n\t\t(on_board yes)\n'
|
||||
'\t\t(property "Reference" "U" (at 0 12.7 0) (effects (font (size 1.27 1.27))))\n'
|
||||
'\t\t(property "Value" "TPS54202" (at 0 -16.51 0) (effects (font (size 1.27 1.27))))\n'
|
||||
'\t\t(property "Footprint" "Package_TO_SOT_SMD:SOT-23-6" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
'\t\t(property "Datasheet" "https://www.ti.com/lit/ds/symlink/tps54202.pdf" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
'\t\t(symbol "TPS54202_0_1"\n'
|
||||
'\t\t\t(rectangle (start -10.16 10.16) (end 10.16 -10.16) (stroke (width 0.254) (type default)) (fill (type background)))\n\t\t)\n'
|
||||
f'\t\t(symbol "TPS54202_1_1"\n{pt}\t\t)\n\t)')
|
||||
LIBS["CM5IO:TPS54202"] = (blk, lib_pins(blk))
|
||||
register_tps54202()
|
||||
# +5v pre-exists in the carrier as a HIERARCHICAL net (local + hierarchical labels on the CM5
|
||||
# sheets) -> expose it hierarchically here (a global label collides: same_local_global_label).
|
||||
# +3V3_RF/+3V3_AUX are new nets introduced by this port -> global is fine.
|
||||
GLOBAL_NETS = {"+3V3_RF", "+3V3_AUX", "PCIE_PWR_EN"}
|
||||
HIER_NETS = {"+5v"}
|
||||
# from KiCad libs (renamed to full lib_id)
|
||||
add_from("power:PWR_FLAG", open(f"{KSS}/power.kicad_sym").read(), "PWR_FLAG")
|
||||
add_from("Power_Management:LM74700", open(f"{KSS}/Power_Management.kicad_sym").read(), "LM74700")
|
||||
add_from("Transistor_FET:Q_NMOS_GDS", open(f"{KSS}/Transistor_FET.kicad_sym").read(), "Q_NMOS_GDS")
|
||||
add_from("Device:D_TVS", open(f"{KSS}/Device.kicad_sym").read(), "D_TVS")
|
||||
add_from("Connector:Screw_Terminal_01x02", open(f"{KSS}/Connector.kicad_sym").read(), "Screw_Terminal_01x02")
|
||||
|
||||
# ---- placement engine -------------------------------------------------------------
|
||||
body = []
|
||||
def emit_label(net, x, y, ang=0):
|
||||
body.append(f'\t(label "{net}"\n\t\t(at {x} {y} {ang})\n'
|
||||
f'\t\t(effects (font (size 1.27 1.27)) (justify left))\n\t\t(uuid "{U("l-"+net+f"-{x}-{y}")}")\n\t)\n')
|
||||
def emit_glabel(net, x, y, ang=0):
|
||||
body.append(f'\t(global_label "{net}"\n\t\t(shape input)\n\t\t(at {x} {y} {ang})\n'
|
||||
f'\t\t(effects (font (size 1.27 1.27)) (justify left))\n\t\t(uuid "{U("gl-"+net+f"-{x}-{y}")}")\n\t)\n')
|
||||
def emit_hlabel(net, x, y, ang=0):
|
||||
body.append(f'\t(hierarchical_label "{net}"\n\t\t(shape passive)\n\t\t(at {x} {y} {ang})\n'
|
||||
f'\t\t(effects (font (size 1.27 1.27)) (justify left))\n\t\t(uuid "{U("hl-"+net+f"-{x}-{y}")}")\n\t)\n')
|
||||
def emit_gnd(x, y, tag):
|
||||
tag = tag if tag[-1].isdigit() else tag + "1" # refs must end in a digit (KiCad annotation)
|
||||
body.append(f'\t(symbol\n\t\t(lib_id "power:GND")\n\t\t(at {x} {y} 0)\n\t\t(unit 1)\n'
|
||||
f'\t\t(exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no)\n\t\t(uuid "{U("gnd-"+tag)}")\n'
|
||||
f'\t\t(property "Reference" "#PWR{tag}" (at {x} {y+3} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
f'\t\t(property "Value" "GND" (at {x} {y+2} 0) (effects (font (size 1.27 1.27))))\n'
|
||||
f'\t\t(property "Footprint" "" (at {x} {y} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
f'\t\t(property "Datasheet" "" (at {x} {y} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
f'\t\t(pin "1" (uuid "{U("gp-"+tag)}"))\n'
|
||||
f'\t\t(instances (project "{PROJECT}" (path "/{ROOT_UUID}/{SHEET_SYMBOL_UUID}" (reference "#PWR{tag}") (unit 1))))\n\t)\n')
|
||||
def emit_flag(x, y, tag):
|
||||
tag = tag if tag[-1].isdigit() else tag + "1" # refs must end in a digit (KiCad annotation)
|
||||
body.append(f'\t(symbol\n\t\t(lib_id "power:PWR_FLAG")\n\t\t(at {x} {y} 0)\n\t\t(unit 1)\n'
|
||||
f'\t\t(exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no)\n\t\t(uuid "{U("flg-"+tag)}")\n'
|
||||
f'\t\t(property "Reference" "#FLG{tag}" (at {x} {y-3} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
f'\t\t(property "Value" "PWR_FLAG" (at {x} {y-2} 0) (effects (font (size 1.27 1.27))))\n'
|
||||
f'\t\t(property "Footprint" "" (at {x} {y} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
f'\t\t(property "Datasheet" "" (at {x} {y} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
f'\t\t(pin "1" (uuid "{U("fp-"+tag)}"))\n'
|
||||
f'\t\t(instances (project "{PROJECT}" (path "/{ROOT_UUID}/{SHEET_SYMBOL_UUID}" (reference "#FLG{tag}") (unit 1))))\n\t)\n')
|
||||
|
||||
def place(lib_id, ref, value, footprint, ox, oy, pin_nets):
|
||||
"""Place a symbol at grid-snapped (ox,oy); attach each pin's net by name.
|
||||
pin_nets: {pinnum: 'NET' | ('gnd',) }. GND pins use a power:GND symbol."""
|
||||
ox, oy = g(ox), g(oy)
|
||||
_, pins = LIBS[lib_id]
|
||||
extra = ('\t\t(property "Description" "" (at %g %g 0) (effects (font (size 1.27 1.27)) (hide yes)))\n' % (ox, oy))
|
||||
sym = (f'\t(symbol\n\t\t(lib_id "{lib_id}")\n\t\t(at {ox} {oy} 0)\n\t\t(unit 1)\n'
|
||||
f'\t\t(exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no)\n\t\t(uuid "{U("sym-"+ref)}")\n'
|
||||
f'\t\t(property "Reference" "{ref}" (at {ox+2.54} {oy-1} 0) (effects (font (size 1.27 1.27)) (justify left)))\n'
|
||||
f'\t\t(property "Value" "{value}" (at {ox+2.54} {oy+1} 0) (effects (font (size 1.27 1.27)) (justify left)))\n'
|
||||
f'\t\t(property "Footprint" "{footprint}" (at {ox} {oy} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
f'\t\t(property "Datasheet" "" (at {ox} {oy} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n' + extra)
|
||||
for num in sorted(pins, key=lambda n: int(re.sub(r'\D', '', n) or 0)):
|
||||
sym += f'\t\t(pin "{num}" (uuid "{U("p-"+ref+"-"+num)}"))\n'
|
||||
sym += f'\t\t(instances (project "{PROJECT}" (path "/{ROOT_UUID}/{SHEET_SYMBOL_UUID}" (reference "{ref}") (unit 1))))\n\t)\n'
|
||||
body.append(sym)
|
||||
for num, net in pin_nets.items():
|
||||
lx, ly, _ = pins[num]; px, py = round(ox + lx, 2), round(oy - ly, 2)
|
||||
if net == ('gnd',): emit_gnd(px, py, ref + num)
|
||||
elif isinstance(net, tuple) and net[0] == 'global': emit_glabel(net[1], px, py)
|
||||
elif net in GLOBAL_NETS: emit_glabel(net, px, py)
|
||||
elif net in HIER_NETS: emit_hlabel(net, px, py)
|
||||
else: emit_label(net, px, py)
|
||||
return ox, oy
|
||||
|
||||
# ---- STAGE 1: input protection ----------------------------------------------------
|
||||
GND = ('gnd',)
|
||||
place("Connector:Screw_Terminal_01x02", "J_PWR1", "15V_DC_IN", "TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-2_1x02_P5.00mm_Horizontal",
|
||||
80, 80, {'1': '+15V_IN', '2': GND})
|
||||
drev = place("Power_Management:LM74700", "D_REV1", "LM74700-Q1", "Package_TO_SOT_SMD:SOT-23-6",
|
||||
120, 80, {'6': '+15V_IN', '3': '+15V_IN', '4': 'VIN_PROT', '5': 'GATE_REV', '1': 'VCAP_REV', '2': GND})
|
||||
place("Transistor_FET:Q_NMOS_GDS", "Q_REV1", "CSD18540 (Vds>=30V)", "Package_TO_SOT_SMD:TO-263-2",
|
||||
140, 90, {'1': 'GATE_REV', '2': 'VIN_PROT', '3': '+15V_IN'})
|
||||
place("Device:C", "C_VCAP1", "1u", "Capacitor_SMD:C_0402_1005Metric", 120, 100, {'1': 'VCAP_REV', '2': GND})
|
||||
tvs = place("Device:D_TVS", "TVS_IN1", "SMBJ18A", "Diode_SMD:D_SMB", 160, 80, {'1': 'VIN_PROT', '2': GND})
|
||||
place("Device:C_Polarized", "C_BULK1", "100u/35V", "Capacitor_SMD:CP_Elec_8x10", 170, 80, {'1': 'VIN_PROT', '2': GND}) # 35V: must survive SMBJ18A clamp (<=29.2V surge)
|
||||
place("Device:C", "C_BULK2", "10u/50V", "Capacitor_SMD:C_1210_3225Metric", 180, 80, {'1': 'VIN_PROT', '2': GND})
|
||||
# PWR_FLAGs co-located exactly on real pins so they bind: +15V_IN @ D_REV.6 (ANODE),
|
||||
# VIN_PROT @ TVS.1 (a power_in driver for VIN_PROT, needed once bucks consume it).
|
||||
def pin_abs(origin, lib_id, num):
|
||||
lx, ly, _ = LIBS[lib_id][1][num]
|
||||
return round(origin[0] + lx, 2), round(origin[1] - ly, 2)
|
||||
fx, fy = pin_abs(drev, "Power_Management:LM74700", "6"); emit_flag(fx, fy, "15IN")
|
||||
vx, vy = pin_abs(tvs, "Device:D_TVS", "1"); emit_flag(vx, vy, "VINP")
|
||||
|
||||
# ---- STAGE 2: U_BUCK5 15V -> +5v 5A (LM61460-Q1, sync, 42V absmax) --------------
|
||||
# FB divider: Vout = VREF*(1+Rfbt/Rfbb), VREF=1.0V -> Rfbt/Rfbb=4 -> 40.2k/10.0k = 5.02V.
|
||||
# fsw ~= 400kHz (RT 31.6k); L=4.7uH; Cout 2x47u/16V; Cin 2x10u/50V; Cboot 100n; BIAS->+5v.
|
||||
place("CM5IO:LM61460-Q1", "U_BUCK5", "LM61460-Q1", "Package_SO:Texas_HTSSOP-14-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask3.155x3.255mm_ThermalVias",
|
||||
110, 150, {'8': 'VIN_PROT', '12': 'VIN_PROT', '7': 'VIN_PROT', '1': '+5v', '2': 'VCC5',
|
||||
'6': 'RT5', '5': 'PG5', '4': 'FB5', '10': 'SW5', '14': 'BOOT5', '13': 'RBOOT5',
|
||||
'9': GND, '11': GND, '3': GND, '15': GND})
|
||||
l5 = place("Device:L", "L5", "4.7uH/7A", "Inductor_SMD:L_Bourns_SRP1245A", 150, 138, {'1': 'SW5', '2': '+5v'})
|
||||
place("Device:C", "C_BOOT5", "100n", "Capacitor_SMD:C_0402_1005Metric", 140, 132, {'1': 'SW5', '2': 'BOOT5'})
|
||||
place("Device:R", "R_BOOT5", "4.7", "Resistor_SMD:R_0402_1005Metric", 145, 128, {'1': 'RBOOT5', '2': 'BOOT5'})
|
||||
place("Device:R", "R_RT5", "31.6k", "Resistor_SMD:R_0402_1005Metric", 90, 158, {'1': 'RT5', '2': GND})
|
||||
place("Device:R", "R_PG5", "100k", "Resistor_SMD:R_0402_1005Metric", 135, 143, {'1': 'PG5', '2': '+5v'})
|
||||
place("Device:R", "R_FBT5", "40.2k", "Resistor_SMD:R_0402_1005Metric", 165, 150, {'1': '+5v', '2': 'FB5'})
|
||||
place("Device:R", "R_FBB5", "10.0k", "Resistor_SMD:R_0402_1005Metric", 165, 158, {'1': 'FB5', '2': GND})
|
||||
place("Device:C", "C_VCC5", "1u", "Capacitor_SMD:C_0402_1005Metric", 100, 158, {'1': 'VCC5', '2': GND})
|
||||
place("Device:C", "C_IN5a1", "10u/50V", "Capacitor_SMD:C_1210_3225Metric", 95, 145, {'1': 'VIN_PROT', '2': GND})
|
||||
place("Device:C", "C_IN5b1", "10u/50V", "Capacitor_SMD:C_1210_3225Metric", 100, 145, {'1': 'VIN_PROT', '2': GND})
|
||||
place("Device:C", "C_OUT5a1", "47u/16V", "Capacitor_SMD:C_1210_3225Metric", 175, 138, {'1': '+5v', '2': GND})
|
||||
place("Device:C", "C_OUT5b1", "47u/16V", "Capacitor_SMD:C_1210_3225Metric", 180, 138, {'1': '+5v', '2': GND})
|
||||
# +5v is now sourced by this buck -> PWR_FLAG co-located on L5 pin2 (+5v)
|
||||
p5x, p5y = pin_abs(l5, "Device:L", "2"); emit_flag(p5x, p5y, "P5V")
|
||||
|
||||
# ---- STAGE 3: U_BUCK33R 15V -> +3V3_RF 4A (LM61460-Q1, sync) --------------------
|
||||
# FB divider: VREF=1.0V -> Rfbt/Rfbb=2.32 -> 23.2k/10.0k = 3.32V.
|
||||
# Same support set as stage 2 (RT 31.6k ~420kHz, L 4.7uH/7A: ripple ~1.3A = 33% of 4A).
|
||||
# BIAS -> +3V3_RF (datasheet: BIAS>=3.1V uses output LDO path; falls back to VIN below).
|
||||
# EN <- PCIE_PWR_EN (CM5 Module1 pin 106, global net) + 100k pull-down (= reference R14)
|
||||
# so the Wi-Fi rail stays OFF until the CM5 asserts it.
|
||||
place("CM5IO:LM61460-Q1", "U_BUCK33R1", "LM61460-Q1", "Package_SO:Texas_HTSSOP-14-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask3.155x3.255mm_ThermalVias",
|
||||
230, 150, {'8': 'VIN_PROT', '12': 'VIN_PROT', '7': 'PCIE_PWR_EN', '1': '+3V3_RF', '2': 'VCC33',
|
||||
'6': 'RT33', '5': 'PG33', '4': 'FB33', '10': 'SW33', '14': 'BOOT33', '13': 'RBOOT33',
|
||||
'9': GND, '11': GND, '3': GND, '15': GND})
|
||||
l33 = place("Device:L", "L33", "4.7uH/7A", "Inductor_SMD:L_Bourns_SRP1245A", 270, 138, {'1': 'SW33', '2': '+3V3_RF'})
|
||||
place("Device:C", "C_BOOT33", "100n", "Capacitor_SMD:C_0402_1005Metric", 260, 132, {'1': 'SW33', '2': 'BOOT33'})
|
||||
place("Device:R", "R_BOOT33", "4.7", "Resistor_SMD:R_0402_1005Metric", 265, 128, {'1': 'RBOOT33', '2': 'BOOT33'})
|
||||
place("Device:R", "R_RT33", "31.6k", "Resistor_SMD:R_0402_1005Metric", 210, 158, {'1': 'RT33', '2': GND})
|
||||
place("Device:R", "R_PG33", "100k", "Resistor_SMD:R_0402_1005Metric", 255, 143, {'1': 'PG33', '2': '+3V3_RF'})
|
||||
place("Device:R", "R_FBT33", "23.2k", "Resistor_SMD:R_0402_1005Metric", 285, 150, {'1': '+3V3_RF', '2': 'FB33'})
|
||||
place("Device:R", "R_FBB33", "10.0k", "Resistor_SMD:R_0402_1005Metric", 285, 158, {'1': 'FB33', '2': GND})
|
||||
place("Device:R", "R_EN33", "100k", "Resistor_SMD:R_0402_1005Metric", 200, 165, {'1': 'PCIE_PWR_EN', '2': GND})
|
||||
place("Device:C", "C_VCC33", "1u", "Capacitor_SMD:C_0402_1005Metric", 220, 158, {'1': 'VCC33', '2': GND})
|
||||
place("Device:C", "C_IN33a1", "10u/50V", "Capacitor_SMD:C_1210_3225Metric", 215, 145, {'1': 'VIN_PROT', '2': GND})
|
||||
place("Device:C", "C_IN33b1", "10u/50V", "Capacitor_SMD:C_1210_3225Metric", 220, 145, {'1': 'VIN_PROT', '2': GND})
|
||||
place("Device:C", "C_OUT33a1", "47u/16V", "Capacitor_SMD:C_1210_3225Metric", 240, 165, {'1': '+3V3_RF', '2': GND})
|
||||
place("Device:C", "C_OUT33b1", "47u/16V", "Capacitor_SMD:C_1210_3225Metric", 245, 165, {'1': '+3V3_RF', '2': GND})
|
||||
# +3V3_RF is now sourced by this buck -> PWR_FLAG on L33 pin2 (replaces the E-key temp flag)
|
||||
p33x, p33y = pin_abs(l33, "Device:L", "2"); emit_flag(p33x, p33y, "P33R")
|
||||
|
||||
# ---- STAGE 4: U_BUCK33A 15V -> +3V3_AUX ~1A (TPS54202, sync, 500kHz fixed) ------
|
||||
# Quiet rail: GPS + PPS Schmitt buffer + IO pull-ups + RTC domain (design doc ~1A).
|
||||
# Abs-max gate PASSED (thin): VIN abs-max 30V vs SMBJ18A clamp <=29.2V at full rated
|
||||
# 600W surge (0.8V margin, transient-only; 28V operating max is not the surge number).
|
||||
# FB per TI's 3.3V table row: R2=100k/R3=22.1k + Cff 56pF -> 0.596*(1+100/22.1)=3.29V.
|
||||
# L=10uH (dI~0.52A, Ipk~1.26A), Cout 2x22uF. EN abs-max is 7V -> NO direct VIN tie;
|
||||
# UVLO divider 866k/110k (Ip=0.7uA/Ih=1.55uA, Ven 1.21/1.19V) -> start 10.1V/stop 8.6V,
|
||||
# EN sits ~1.9V at 15V in (~3.5V at 29V surge clamp, still < 7V abs-max).
|
||||
place("CM5IO:TPS54202", "U_BUCK33A1", "TPS54202", "Package_TO_SOT_SMD:SOT-23-6",
|
||||
230, 95, {'3': 'VIN_PROT', '5': 'EN33A', '6': 'BOOT33A', '2': 'SW33A',
|
||||
'4': 'FB33A', '1': GND})
|
||||
l33a = place("Device:L", "L33A1", "10uH/2.5A", "Inductor_SMD:L_1210_3225Metric", 260, 85, {'1': 'SW33A', '2': '+3V3_AUX'})
|
||||
place("Device:C", "C_BOOT33A1", "100n", "Capacitor_SMD:C_0402_1005Metric", 250, 78, {'1': 'SW33A', '2': 'BOOT33A'})
|
||||
place("Device:R", "R_UVT33A1", "866k", "Resistor_SMD:R_0402_1005Metric", 205, 85, {'1': 'VIN_PROT', '2': 'EN33A'})
|
||||
place("Device:R", "R_UVB33A1", "110k", "Resistor_SMD:R_0402_1005Metric", 205, 95, {'1': 'EN33A', '2': GND})
|
||||
place("Device:R", "R_FBT33A1", "100k", "Resistor_SMD:R_0402_1005Metric", 280, 95, {'1': '+3V3_AUX', '2': 'FB33A'})
|
||||
place("Device:C", "C_FF33A1", "56p", "Capacitor_SMD:C_0402_1005Metric", 288, 95, {'1': '+3V3_AUX', '2': 'FB33A'})
|
||||
place("Device:R", "R_FBB33A1", "22.1k", "Resistor_SMD:R_0402_1005Metric", 280, 105, {'1': 'FB33A', '2': GND})
|
||||
# NB refdes must not differ from stage-3's C_IN33a by case alone — SPICE names are
|
||||
# case-insensitive and the simulate_subcircuits testbench collides ("device already exists").
|
||||
place("Device:C", "C_IN33Aa1", "10u/50V", "Capacitor_SMD:C_1210_3225Metric", 215, 105, {'1': 'VIN_PROT', '2': GND})
|
||||
place("Device:C", "C_INHF33A1", "100n", "Capacitor_SMD:C_0402_1005Metric", 220, 105, {'1': 'VIN_PROT', '2': GND})
|
||||
place("Device:C", "C_OUT33Aa1", "22u/16V", "Capacitor_SMD:C_1210_3225Metric", 265, 105, {'1': '+3V3_AUX', '2': GND})
|
||||
place("Device:C", "C_OUT33Ab1", "22u/16V", "Capacitor_SMD:C_1210_3225Metric", 270, 105, {'1': '+3V3_AUX', '2': GND})
|
||||
# +3V3_AUX sourced here -> PWR_FLAG on L33A pin2
|
||||
p33ax, p33ay = pin_abs(l33a, "Device:L", "2"); emit_flag(p33ax, p33ay, "P33A")
|
||||
|
||||
# ---- file scaffold ----------------------------------------------------------------
|
||||
libsyms = "\n".join(blk for blk, _ in LIBS.values())
|
||||
sheet = ('(kicad_sch\n\t(version 20231120)\n\t(generator "eeschema")\n\t(generator_version "8.0")\n'
|
||||
f'\t(uuid "{SHEET_FILE_UUID}")\n\t(paper "A4")\n'
|
||||
'\t(title_block\n\t\t(title "CM5 Carrier - Power")\n\t\t(rev "A")\n'
|
||||
'\t\t(comment 1 "15V inlet -> ideal-diode -> VIN_PROT -> 3 bucks (+5V, +3V3_RF, +3V3_AUX)")\n\t)\n'
|
||||
'\t(lib_symbols\n' + libsyms + '\n\t)\n' + "".join(body) +
|
||||
'\t(sheet_instances\n\t\t(path "/"\n\t\t\t(page "5")\n\t\t)\n\t)\n)\n')
|
||||
assert sheet.count('(') == sheet.count(')'), f"UNBALANCED {sheet.count('(')} vs {sheet.count(')')}"
|
||||
print("sheet-symbol uuid:", SHEET_SYMBOL_UUID)
|
||||
print("parens balanced:", sheet.count('('))
|
||||
out = os.path.join(CARRIER, "Power.kicad_sch")
|
||||
if '--apply' in sys.argv: open(out, 'w').write(sheet); print("WROTE " + out)
|
||||
else: print("DRY RUN (--apply to write)")
|
||||
64
tools/install-kicad-toolkit
Executable file
64
tools/install-kicad-toolkit
Executable file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
# install-kicad-toolkit — install circuit_toolkit into KiCad's bundled Python
|
||||
# so pcbnew-dependent builders (pcb.py) can import it.
|
||||
#
|
||||
# Usage: ./install-kicad-toolkit.sh
|
||||
#
|
||||
# This is a one-time setup. After KiCad updates, re-run to pick up changes.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
TOOLKIT_SRC="$PROJECT_ROOT/kicad-claude-toolkit/python/circuit_toolkit"
|
||||
|
||||
# Find KiCad Python
|
||||
KICAD_APP="/Applications/KiCad/KiCad.app"
|
||||
KICAD_PYTHON="${KICAD_APP}/Contents/Frameworks/Python.framework/Versions/3.9/bin/python3"
|
||||
|
||||
if [ ! -x "$KICAD_PYTHON" ]; then
|
||||
echo "ERROR: KiCad Python not found at $KICAD_PYTHON" >&2
|
||||
echo "Install KiCad from kicad.org, then re-run." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "KiCad Python: $KICAD_PYTHON"
|
||||
echo "circuit_toolkit source: $TOOLKIT_SRC"
|
||||
echo ""
|
||||
|
||||
# Verify pcbnew works
|
||||
echo "Checking pcbnew..."
|
||||
if ! "$KICAD_PYTHON" -c "import pcbnew" 2>/dev/null; then
|
||||
echo "ERROR: pcbnew not importable. Run KiCad.app at least once." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " pcbnew OK"
|
||||
|
||||
# Install circuit_toolkit in development mode
|
||||
echo ""
|
||||
echo "Installing circuit_toolkit (editable)..."
|
||||
pushd "$TOOLKIT_SRC" >/dev/null
|
||||
# KiCad ships Python 3.9; toolkit pyproject requires >=3.10. Use --no-build-isolation
|
||||
# to bypass the version check (the code only needs 3.9 features).
|
||||
"$KICAD_PYTHON" -m pip install --no-build-isolation -e . 2>&1
|
||||
echo " installed"
|
||||
|
||||
# Also install optional sim deps
|
||||
echo ""
|
||||
echo "Installing optional sim deps (matplotlib, numpy)..."
|
||||
"$KICAD_PYTHON" -m pip install matplotlib numpy 2>&1 || echo " (non-fatal: sim deps skipped)"
|
||||
|
||||
# Verify
|
||||
echo ""
|
||||
echo "Verifying..."
|
||||
"$KICAD_PYTHON" -c "
|
||||
import pcbnew
|
||||
import circuit_toolkit
|
||||
from circuit_toolkit.core.board import Board
|
||||
from circuit_toolkit.builders.pcb import build_pcb
|
||||
print(f' circuit_toolkit {circuit_toolkit.__version__} OK (pcbnew {pcbnew.__file__})')
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "Done. circuit_toolkit is now available inside KiCad's Python."
|
||||
echo "Use ./tools/kicad-python to run scripts with pcbnew access."
|
||||
256
tools/kicad-build
Executable file
256
tools/kicad-build
Executable file
@@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env bash
|
||||
# kicad-build — orchestrator for circuit_toolkit + kicad-cli.
|
||||
#
|
||||
# Usage:
|
||||
# ./kicad-build <board-dir> [--erc] [--drc] [--gerber] [--bom] [--schematic] [--sim] [--all]
|
||||
#
|
||||
# Steps (all are optional, --all runs everything):
|
||||
# 1. circuit_toolkit build (generates .kicad_sch + .kicad_pcb)
|
||||
# 2. kicad-cli sch erc (electrical rules check)
|
||||
# 3. kicad-cli pcb drc (design rules check)
|
||||
# 4. kicad-cli pcb export gerber/drill/position
|
||||
# 5. kicad-cli sch export bom/pdf/netlist
|
||||
# 6. SPICE simulation (if sim/ present)
|
||||
#
|
||||
# Requires:
|
||||
# - KiCad.app installed (for pcbnew)
|
||||
# - kicad-cli on PATH (homebrew: brew install kicad)
|
||||
# - ngspice on PATH (for --sim, brew install ngspice)
|
||||
# - netlistsvg on PATH (for --schematic SVG, npm install -g netlistsvg)
|
||||
# - circuit_toolkit installed into KiCad's Python (run install-kicad-toolkit.sh)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
KICAD_PYTHON="$SCRIPT_DIR/kicad-python"
|
||||
KICAD_CLI=$(command -v kicad-cli 2>/dev/null || echo "")
|
||||
|
||||
# circuit_toolkit paths
|
||||
TOOLKIT_SRC="$PROJECT_ROOT/kicad-claude-toolkit/python/circuit_toolkit"
|
||||
TOOLKIT_PY="$TOOLKIT_SRC/circuit_toolkit"
|
||||
|
||||
# Output directory (relative to board dir)
|
||||
OUTPUT_DIR=""
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <board-dir> [--erc] [--drc] [--gerber] [--bom] [--schematic] [--sim] [--all]"
|
||||
echo ""
|
||||
echo " --erc Run ERC on generated schematic"
|
||||
echo " --drc Run DRC on generated PCB"
|
||||
echo " --gerber Export Gerber + drill + position files"
|
||||
echo " --bom Generate BOM (flat + JLCPCB)"
|
||||
echo " --schematic Generate schematic SVG via netlistsvg"
|
||||
echo " --sim Run SPICE simulations (if sim/ present)"
|
||||
echo " --all Run all steps (default)"
|
||||
echo " --dry-run Print planned commands without executing"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse args
|
||||
BOARD_DIR=""
|
||||
DO_ERC=false
|
||||
DO_DRC=false
|
||||
DO_GERBER=false
|
||||
DO_BOM=false
|
||||
DO_SCHEMATIC=false
|
||||
DO_SIM=false
|
||||
DO_ALL=true
|
||||
DRY_RUN=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--erc) DO_ERC=true; DO_ALL=false;;
|
||||
--drc) DO_DRC=true; DO_ALL=false;;
|
||||
--gerber) DO_GERBER=true; DO_ALL=false;;
|
||||
--bom) DO_BOM=true; DO_ALL=false;;
|
||||
--schematic) DO_SCHEMATIC=true; DO_ALL=false;;
|
||||
--sim) DO_SIM=true; DO_ALL=false;;
|
||||
--dry-run) DRY_RUN=true;;
|
||||
-h|--help) usage;;
|
||||
-*)
|
||||
echo "Unknown option: $arg" >&2
|
||||
usage;;
|
||||
*)
|
||||
if [ -z "$BOARD_DIR" ]; then
|
||||
BOARD_DIR="$arg"
|
||||
else
|
||||
echo "Unexpected argument: $arg" >&2
|
||||
usage
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$BOARD_DIR" ]; then
|
||||
echo "ERROR: board directory required" >&2
|
||||
usage
|
||||
fi
|
||||
|
||||
BOARD_DIR="$(cd "$BOARD_DIR" && pwd)"
|
||||
OUTPUT_DIR="$BOARD_DIR/output"
|
||||
|
||||
if $DO_ALL; then
|
||||
DO_ERC=true; DO_DRC=true; DO_GERBER=true; DO_BOM=true; DO_SCHEMATIC=true
|
||||
fi
|
||||
|
||||
# ── Pre-flight checks ──────────────────────────────────────────────────
|
||||
check_kicad_python() {
|
||||
if ! $DRY_RUN && ! bash "$KICAD_PYTHON" -c "import pcbnew" 2>/dev/null; then
|
||||
echo "ERROR: pcbnew not available. Run KiCad.app at least once, then retry." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_kicad_cli() {
|
||||
if [ -z "$KICAD_CLI" ]; then
|
||||
echo "ERROR: kicad-cli not on PATH. Install via: brew install kicad" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Step 1: circuit_toolkit build ──────────────────────────────────────
|
||||
step_build() {
|
||||
local build_script="$BOARD_DIR/build.py"
|
||||
if [ ! -f "$build_script" ]; then
|
||||
echo "SKIP build: no build.py in $BOARD_DIR"
|
||||
return 0
|
||||
fi
|
||||
echo "=== Step 1: circuit_toolkit build ==="
|
||||
if $DRY_RUN; then
|
||||
echo " [dry-run] KiCad Python $KICAD_PYTHON"
|
||||
echo " [dry-run] cd $BOARD_DIR && $KICAD_PYTHON build.py"
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
pushd "$BOARD_DIR" >/dev/null
|
||||
PYTHONPATH="$TOOLKIT_SRC:$PYTHONPATH" \
|
||||
bash "$KICAD_PYTHON" build.py
|
||||
popd >/dev/null
|
||||
echo " -> $OUTPUT_DIR/"
|
||||
}
|
||||
|
||||
# ── Step 2: ERC ────────────────────────────────────────────────────────
|
||||
step_erc() {
|
||||
local sch="$BOARD_DIR/*.kicad_sch"
|
||||
sch=$(ls $sch 2>/dev/null | head -1 || true)
|
||||
if [ -z "$sch" ]; then
|
||||
echo "SKIP ERC: no .kicad_sch found"
|
||||
return 0
|
||||
fi
|
||||
echo "=== Step 2: ERC ==="
|
||||
if $DRY_RUN; then
|
||||
echo " [dry-run] $KICAD_CLI sch erc $sch"
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "$OUTPUT_DIR/erc"
|
||||
$KICAD_CLI sch erc "$sch" --output "$OUTPUT_DIR/erc/erc_report.txt" 2>&1 || true
|
||||
echo " -> $OUTPUT_DIR/erc/"
|
||||
}
|
||||
|
||||
# ── Step 3: DRC ────────────────────────────────────────────────────────
|
||||
step_drc() {
|
||||
local pcb="$BOARD_DIR/*.kicad_pcb"
|
||||
pcb=$(ls $pcb 2>/dev/null | head -1 || true)
|
||||
if [ -z "$pcb" ]; then
|
||||
echo "SKIP DRC: no .kicad_pcb found"
|
||||
return 0
|
||||
fi
|
||||
echo "=== Step 3: DRC ==="
|
||||
if $DRY_RUN; then
|
||||
echo " [dry-run] $KICAD_CLI pcb drc $pcb"
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "$OUTPUT_DIR/drc"
|
||||
$KICAD_CLI pcb drc "$pcb" --output "$OUTPUT_DIR/drc/drc_report.html" 2>&1 || true
|
||||
echo " -> $OUTPUT_DIR/drc/"
|
||||
}
|
||||
|
||||
# ── Step 4: Gerber/Drill/Position export ──────────────────────────────
|
||||
step_gerber() {
|
||||
local pcb="$BOARD_DIR/*.kicad_pcb"
|
||||
pcb=$(ls $pcb 2>/dev/null | head -1 || true)
|
||||
if [ -z "$pcb" ]; then
|
||||
echo "SKIP gerber: no .kicad_pcb found"
|
||||
return 0
|
||||
fi
|
||||
echo "=== Step 4: Gerber + Drill + Position export ==="
|
||||
if $DRY_RUN; then
|
||||
echo " [dry-run] $KICAD_CLI pcb export gerbers $pcb --output $OUTPUT_DIR/fab/gerber"
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "$OUTPUT_DIR/fab/gerber"
|
||||
$KICAD_CLI pcb export gerbers "$pcb" --output "$OUTPUT_DIR/fab/gerber" 2>&1
|
||||
$KICAD_CLI pcb export drill "$pcb" --output "$OUTPUT_DIR/fab/gerber" 2>&1
|
||||
$KICAD_CLI pcb export pos "$pcb" --output "$OUTPUT_DIR/fab/positions.csv" 2>&1
|
||||
echo " -> $OUTPUT_DIR/fab/"
|
||||
}
|
||||
|
||||
# ── Step 5: BOM + schematic PDF + netlist ─────────────────────────────
|
||||
step_bom() {
|
||||
local sch="$BOARD_DIR/*.kicad_sch"
|
||||
sch=$(ls $sch 2>/dev/null | head -1 || true)
|
||||
if [ -z "$sch" ]; then
|
||||
echo "SKIP bom: no .kicad_sch found"
|
||||
return 0
|
||||
fi
|
||||
echo "=== Step 5: BOM + schematic PDF + netlist ==="
|
||||
if $DRY_RUN; then
|
||||
echo " [dry-run] $KICAD_CLI sch export bom $sch --output $OUTPUT_DIR/fab"
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "$OUTPUT_DIR/fab"
|
||||
$KICAD_CLI sch export bom "$sch" --output "$OUTPUT_DIR/fab" 2>&1
|
||||
$KICAD_CLI sch export pdf "$sch" --output "$OUTPUT_DIR/schematic.pdf" 2>&1
|
||||
$KICAD_CLI sch export "$sch" --format netlist --output "$OUTPUT_DIR/fab/netlist.xml" 2>&1
|
||||
echo " -> $OUTPUT_DIR/fab/"
|
||||
}
|
||||
|
||||
# ── Step 6: SPICE simulation ──────────────────────────────────────────
|
||||
step_sim() {
|
||||
if [ ! -d "$BOARD_DIR/sim" ]; then
|
||||
echo "SKIP sim: no sim/ directory"
|
||||
return 0
|
||||
fi
|
||||
echo "=== Step 6: SPICE simulation ==="
|
||||
if $DRY_RUN; then
|
||||
echo " [dry-run] ngspice -b $BOARD_DIR/sim/*.cir"
|
||||
return 0
|
||||
fi
|
||||
if ! command -v ngspice &>/dev/null; then
|
||||
echo "SKIP sim: ngspice not on PATH (brew install ngspice)"
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "$OUTPUT_DIR/sim"
|
||||
for cir in "$BOARD_DIR"/sim/*.cir; do
|
||||
echo " sim: $(basename "$cir")"
|
||||
ngspice -b -r "$OUTPUT_DIR/sim/$(basename "$cir" .cir).raw" "$cir" 2>&1 || true
|
||||
done
|
||||
echo " -> $OUTPUT_DIR/sim/"
|
||||
}
|
||||
|
||||
# ── Run ────────────────────────────────────────────────────────────────
|
||||
echo "kicad-build: $BOARD_DIR"
|
||||
echo " KiCad Python: $KICAD_PYTHON"
|
||||
echo " kicad-cli: $KICAD_CLI"
|
||||
echo " output: $OUTPUT_DIR"
|
||||
echo ""
|
||||
|
||||
check_kicad_python
|
||||
if [ -n "$KICAD_CLI" ]; then
|
||||
: # ok
|
||||
else
|
||||
echo "WARNING: kicad-cli not found (ERC/DRC/gerber/bom will be skipped)"
|
||||
fi
|
||||
|
||||
step_build
|
||||
$DO_ERC && step_erc
|
||||
$DO_DRC && step_drc
|
||||
$DO_GERBER && step_gerber
|
||||
$DO_BOM && step_bom
|
||||
$DO_SIM && step_sim
|
||||
|
||||
echo ""
|
||||
echo "=== Done ==="
|
||||
echo "Output: $OUTPUT_DIR/"
|
||||
ls -la "$OUTPUT_DIR/" 2>/dev/null || true
|
||||
44
tools/kicad-python
Executable file
44
tools/kicad-python
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# kicad-python — launcher for KiCad's bundled Python with pcbnew available.
|
||||
#
|
||||
# Usage:
|
||||
# ./kicad-python <script.py> [args...]
|
||||
# ./kicad-python -c "import pcbnew; ..."
|
||||
# ./kicad-python -m circuit_toolkit.build my-board/
|
||||
#
|
||||
# Detects KiCad.app on macOS, KiCad on Linux, or KiCad Flatpak.
|
||||
# Exits non-zero if KiCad Python is not found.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
KICAD_APP="/Applications/KiCad/KiCad.app"
|
||||
KICAD_PYTHON="${KICAD_APP}/Contents/Frameworks/Python.framework/Versions/3.9/bin/python3"
|
||||
|
||||
# Try Flatpak install
|
||||
if [ -x "/var/lib/flatpak/app/org.kicad.KiCad" ] || command -v flatpak &>/dev/null; then
|
||||
FLATPAK_PYTHON=$(flatpak run --command=python3 org.kicad.KiCad 2>/dev/null || true)
|
||||
if [ -n "$FLATPAK_PYTHON" ]; then
|
||||
KICAD_PYTHON="$FLATPAK_PYTHON"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Try Linux install
|
||||
if [ ! -x "$KICAD_PYTHON" ]; then
|
||||
for candidate in \
|
||||
/opt/kicad/bin/python3 \
|
||||
/usr/bin/kicad-python \
|
||||
/usr/bin/python3; do
|
||||
if "$candidate" -c "import pcbnew" 2>/dev/null; then
|
||||
KICAD_PYTHON="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ ! -x "$KICAD_PYTHON" ]; then
|
||||
echo "ERROR: KiCad Python not found at $KICAD_PYTHON" >&2
|
||||
echo "Install KiCad (kicad.org) or a Flatpak, then re-run." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec "$KICAD_PYTHON" "$@"
|
||||
288
tools/retire_block.py
Normal file
288
tools/retire_block.py
Normal file
@@ -0,0 +1,288 @@
|
||||
#!/usr/bin/env python3
|
||||
"""retire_block.py — remove a circuit block from a CM5-carrier KiCad sheet cleanly.
|
||||
|
||||
For the diff-from-reference port (see .claude/skills/kicad-port). Given a sheet and a
|
||||
set of reference designators to drop plus the net-name globs they own, this:
|
||||
1. excises each named (symbol ...) block (paren-matched, by Reference property),
|
||||
2. floods the retired nets from their (label ...) coords through (wire ...) segments,
|
||||
3. removes those labels + every wire in the flooded nets,
|
||||
4. places (no_connect ...) markers on the exposed module pins that sat on those nets,
|
||||
5. writes back only if parentheses stay balanced.
|
||||
|
||||
Module pin coordinates are computed from the lib_symbol pin locals + the placement
|
||||
transform (validated abs = origin + (lx, -ly) for rot0/no-mirror; other orientations
|
||||
are rejected rather than guessed). Idempotent-ish: re-running finds nothing to do.
|
||||
|
||||
Usage:
|
||||
python3 tools/retire_block.py <sheet.kicad_sch> \
|
||||
--symbols J7,U18 --nets 'SD_*' \
|
||||
--module-lib CM5IO:ComputeModule5-CM5 [--apply]
|
||||
Without --apply it's a dry run (prints the plan, writes nothing).
|
||||
"""
|
||||
import re, sys, argparse, fnmatch, math, uuid as uuidlib
|
||||
|
||||
S = r'\s*'
|
||||
|
||||
def xform(px, py, rot, mirror, lx, ly):
|
||||
"""Schematic abs coord of a lib pin local (lx,ly) for a placement. Validated:
|
||||
mirror, then CCW rotate, then lib-Y-up -> schematic-Y-down flip."""
|
||||
X, Y = lx, ly
|
||||
if mirror == 'y': X = -X
|
||||
if mirror == 'x': Y = -Y
|
||||
r = math.radians(rot)
|
||||
rx = X * math.cos(r) - Y * math.sin(r)
|
||||
ry = X * math.sin(r) + Y * math.cos(r)
|
||||
return (round(px + rx, 2), round(py - ry, 2))
|
||||
|
||||
def paren_end(t, s):
|
||||
d = 0
|
||||
for i in range(s, len(t)):
|
||||
if t[i] == '(': d += 1
|
||||
elif t[i] == ')':
|
||||
d -= 1
|
||||
if d == 0: return i + 1
|
||||
raise ValueError("unbalanced from offset %d" % s)
|
||||
|
||||
def fnum(x): return round(float(x), 2)
|
||||
|
||||
def find_symbol_blocks(t):
|
||||
"""Yield (start,end,reference) for every placed (symbol (lib_id ...)) block."""
|
||||
for m in re.finditer(r'\(symbol' + S + r'\(lib_id' + S + r'"', t):
|
||||
s = m.start(); e = paren_end(t, s); seg = t[s:e]
|
||||
ref = re.search(r'\(property' + S + r'"Reference"' + S + r'"([^"]+)"', seg)
|
||||
if ref: yield (s, e, ref.group(1))
|
||||
|
||||
def module_pins_abs(t, module_lib):
|
||||
"""Compute {pin_number:(x,y)} for the placed module instance on this sheet."""
|
||||
inst = re.search(r'\(symbol' + S + r'\(lib_id' + S + r'"' + re.escape(module_lib) + r'"\)(.{0,500}?)\(unit' + S + r'(\d+)\)', t, re.S)
|
||||
if not inst: return {}, None
|
||||
body, unit = inst.group(1), inst.group(2)
|
||||
at = re.search(r'\(at' + S + r'([\d.-]+)' + S + r'([\d.-]+)' + S + r'([\d.-]+)\)', body)
|
||||
mir = re.search(r'\(mirror' + S + r'(\w+)\)', body)
|
||||
px, py, rot = float(at.group(1)), float(at.group(2)), float(at.group(3))
|
||||
if rot != 0 or mir:
|
||||
sys.exit(f"REFUSING: module instance has rot={rot} mirror={mir and mir.group(1)} — transform only validated for rot0/no-mirror.")
|
||||
libname = module_lib.split(':')[-1]
|
||||
pins = {}
|
||||
for pm in re.finditer(r'\(pin\b.*?\(at' + S + r'([\d.-]+)' + S + r'([\d.-]+)' + S + r'[\d.-]+\).*?\(number' + S + r'"([^"]+)"', t, re.S):
|
||||
lx, ly, num = float(pm.group(1)), float(pm.group(2)), pm.group(3)
|
||||
pins.setdefault(num, (round(px + lx, 2), round(py - ly, 2)))
|
||||
return pins, unit
|
||||
|
||||
def collect(t, pat):
|
||||
"""Return list of (start,end,coord,text) for top-level objects named `pat` token."""
|
||||
out = []
|
||||
for m in re.finditer(r'\(' + pat + r'\b', t):
|
||||
s = m.start(); e = paren_end(t, s); seg = t[s:e]
|
||||
at = re.search(r'\(at' + S + r'([\d.-]+)' + S + r'([\d.-]+)', seg)
|
||||
out.append((s, e, (fnum(at.group(1)), fnum(at.group(2))) if at else None, seg))
|
||||
return out
|
||||
|
||||
def wire_pts(seg):
|
||||
return [(fnum(a), fnum(b)) for a, b in re.findall(r'\(xy' + S + r'([\d.-]+)' + S + r'([\d.-]+)\)', seg)]
|
||||
|
||||
def lib_pins(t, lib_id):
|
||||
"""Local (lx,ly,angle) for every pin of a lib_symbol definition."""
|
||||
name = lib_id.split(':')[-1]
|
||||
lm = re.search(r'\(symbol' + S + r'"' + re.escape(lib_id) + r'"', t) or \
|
||||
re.search(r'\(symbol' + S + r'"' + re.escape(name) + r'"', t)
|
||||
if not lm: return []
|
||||
seg = t[lm.start():paren_end(t, lm.start())]
|
||||
return [(float(a), float(b), float(c)) for a, b, c in
|
||||
re.findall(r'\(pin\b.*?\(at' + S + r'([\d.-]+)' + S + r'([\d.-]+)' + S + r'([\d.-]+)\)', seg, re.S)]
|
||||
|
||||
def placed_symbols(t):
|
||||
"""Yield dict per placed symbol: start,end,ref,lib,at(px,py,rot),mirror."""
|
||||
for m in re.finditer(r'\(symbol' + S + r'\(lib_id' + S + r'"([^"]+)"\)', t):
|
||||
s = m.start(); e = paren_end(t, s); seg = t[s:e]
|
||||
ref = re.search(r'\(property' + S + r'"Reference"' + S + r'"([^"]+)"', seg)
|
||||
at = re.search(r'\(at' + S + r'([\d.-]+)' + S + r'([\d.-]+)' + S + r'([\d.-]+)\)', seg)
|
||||
mir = re.search(r'\(mirror' + S + r'(\w+)\)', seg)
|
||||
yield dict(s=s, e=e, ref=ref.group(1) if ref else None, lib=m.group(1),
|
||||
at=(float(at.group(1)), float(at.group(2)), float(at.group(3))) if at else None,
|
||||
mirror=mir.group(1) if mir else None)
|
||||
|
||||
def sym_pin_coords(t, sym):
|
||||
if not sym['at']: return []
|
||||
px, py, rot = sym['at']
|
||||
return [xform(px, py, rot, sym['mirror'], lx, ly) for lx, ly, _ in lib_pins(t, sym['lib'])]
|
||||
|
||||
def drop_sheet_spans(t, fname):
|
||||
"""Remove an entire (sheet ...) block by its Sheetfile property, plus any
|
||||
(path ...) in (sheet_instances) referencing the sheet's UUID. Run on the parent
|
||||
(root) sheet. Returns list of (start,end) spans to delete."""
|
||||
spans = []
|
||||
for m in re.finditer(r'\(sheet\b', t):
|
||||
s = m.start(); e = paren_end(t, s); seg = t[s:e]
|
||||
sf = re.search(r'Sheetfile"' + S + r'"([^"]+)"', seg)
|
||||
if not sf or sf.group(1) != fname: continue
|
||||
spans.append((s, e))
|
||||
u = re.search(r'\(uuid' + S + r'"([0-9a-fA-F-]+)"', seg)
|
||||
if u:
|
||||
si = re.search(r'\(sheet_instances\b', t)
|
||||
if si:
|
||||
ss = si.start(); se = paren_end(t, ss)
|
||||
for pm in re.finditer(r'\(path' + S + r'"[^"]*' + re.escape(u.group(1)) + r'[^"]*"', t[ss:se]):
|
||||
ps = ss + pm.start(); spans.append((ps, paren_end(t, ps)))
|
||||
return spans
|
||||
|
||||
def sheet_port_spans(t, names):
|
||||
"""On a ROOT sheet, find (pin "NAME" ...) hierarchical ports inside (sheet ...) blocks
|
||||
whose name is in `names`. Returns (pin spans to remove, their connection coords)."""
|
||||
spans, coords = [], set()
|
||||
for m in re.finditer(r'\(sheet\b', t):
|
||||
s = m.start(); e = paren_end(t, s)
|
||||
for pm in re.finditer(r'\(pin' + S + r'"([^"]+)"', t[s:e]):
|
||||
if pm.group(1) in names:
|
||||
ps = s + pm.start(); pe = paren_end(t, ps)
|
||||
at = re.search(r'\(at' + S + r'([\d.-]+)' + S + r'([\d.-]+)', t[ps:pe])
|
||||
spans.append((ps, pe))
|
||||
if at: coords.add((fnum(at.group(1)), fnum(at.group(2))))
|
||||
return spans, coords
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('sheet')
|
||||
ap.add_argument('--symbols', default='')
|
||||
ap.add_argument('--nets', default='')
|
||||
ap.add_argument('--nc-pins', default='', help='module pin NUMBERS to no-connect (from analyzer regressions)')
|
||||
ap.add_argument('--sheet-ports', default='', help='hierarchical net names: remove matching sheet-symbol pins + connecting root wires (run on the ROOT sheet)')
|
||||
ap.add_argument('--drop-sheet', default='', help='Sheetfile name: remove the whole (sheet ...) block + its sheet_instances path (run on the ROOT/parent sheet)')
|
||||
ap.add_argument('--module-lib', default='CM5IO:ComputeModule5-CM5')
|
||||
ap.add_argument('--apply', action='store_true')
|
||||
a = ap.parse_args()
|
||||
drop_refs = set(filter(None, a.symbols.split(',')))
|
||||
net_globs = list(filter(None, a.nets.split(',')))
|
||||
nc_nums = list(filter(None, a.nc_pins.split(',')))
|
||||
port_names = set(filter(None, a.sheet_ports.split(',')))
|
||||
t = open(a.sheet).read()
|
||||
|
||||
modpins, unit = module_pins_abs(t, a.module_lib)
|
||||
pin_at = {xy: n for n, xy in modpins.items()}
|
||||
nc_pins = [(n, modpins[n]) for n in nc_nums if n in modpins]
|
||||
missing_nc = [n for n in nc_nums if n not in modpins]
|
||||
if missing_nc: sys.exit(f"NC pins not found on module: {missing_nc}")
|
||||
|
||||
# 1) symbols to remove + their abs pin coords (for stub cleanup)
|
||||
placed = list(placed_symbols(t))
|
||||
removed_syms = [sy for sy in placed if sy['ref'] in drop_refs]
|
||||
found_refs = {sy['ref'] for sy in removed_syms}
|
||||
sym_spans = [(sy['s'], sy['e']) for sy in removed_syms]
|
||||
removed_pin_coords = set()
|
||||
for sy in removed_syms:
|
||||
removed_pin_coords |= set(sym_pin_coords(t, sy))
|
||||
|
||||
# 2) retired labels by glob — match ANY label type (local / hierarchical / global)
|
||||
labels = collect(t, 'label') + collect(t, 'hierarchical_label') + collect(t, 'global_label')
|
||||
retired_labels = [(s, e, c) for s, e, c, seg in labels
|
||||
for nm in [re.search(r'"([^"]+)"', seg).group(1)]
|
||||
if any(fnmatch.fnmatch(nm, g) for g in net_globs)]
|
||||
retired_coords = {c for _, _, c in retired_labels}
|
||||
|
||||
# 3) anchor-aware wire removal via connected components.
|
||||
# A wire is removed only if its component (a) is orphaned by THIS edit (touches a
|
||||
# removed pin, retired label, or target NC pin) AND (b) reaches no KEPT anchor —
|
||||
# a kept non-power component pin or a kept label. Shared nets (e.g. a camera FFC's
|
||||
# I2C/power pins) stay anchored by the kept side, so they survive; pre-existing
|
||||
# floating wires aren't seeded, so they're untouched.
|
||||
wires = collect(t, 'wire')
|
||||
wpts = [(s, e, wire_pts(seg)) for s, e, _, seg in wires]
|
||||
parent = {}
|
||||
def find(x):
|
||||
parent.setdefault(x, x)
|
||||
root = x
|
||||
while parent[root] != root: root = parent[root]
|
||||
while parent[x] != root: parent[x], x = root, parent[x]
|
||||
return root
|
||||
def union(a, b): parent[find(a)] = find(b)
|
||||
for s, e, pts in wpts:
|
||||
for p in pts[1:]:
|
||||
union(pts[0], p)
|
||||
|
||||
nc_coords = {xy for _, xy in nc_pins}
|
||||
kept_label_coords = {c for s, e, c, seg in labels
|
||||
if not any(fnmatch.fnmatch(re.search(r'"([^"]+)"', seg).group(1), g)
|
||||
for g in net_globs)}
|
||||
kept_pin_coords = set()
|
||||
for sy in placed:
|
||||
if sy['ref'] in drop_refs or sy['lib'].startswith('power:'): continue
|
||||
kept_pin_coords |= set(sym_pin_coords(t, sy))
|
||||
kept_pin_coords -= nc_coords # pins we are NC'ing are being disconnected
|
||||
anchors = kept_pin_coords | kept_label_coords
|
||||
# sheet-symbol hierarchical ports to drop (root sheet) + their connection coords
|
||||
port_spans, port_coords = sheet_port_spans(t, port_names) if port_names else ([], set())
|
||||
sheet_spans = drop_sheet_spans(t, a.drop_sheet) if a.drop_sheet else []
|
||||
seeds = set(removed_pin_coords) | set(retired_coords) | nc_coords | port_coords
|
||||
|
||||
anchored_roots, seeded_roots = set(), set()
|
||||
for c in anchors:
|
||||
if c in parent: anchored_roots.add(find(c))
|
||||
for c in seeds:
|
||||
if c in parent: seeded_roots.add(find(c))
|
||||
removable_roots = seeded_roots - anchored_roots
|
||||
wire_remove = [(s, e, pts) for s, e, pts in wpts if find(pts[0]) in removable_roots]
|
||||
wire_remove_spans = {(s, e) for s, e, _ in wire_remove}
|
||||
freed = set(removed_pin_coords)
|
||||
for _, _, pts in wire_remove:
|
||||
freed |= set(pts)
|
||||
kept_wire_pts = set()
|
||||
for s, e, pts in wpts:
|
||||
if (s, e) not in wire_remove_spans:
|
||||
kept_wire_pts |= set(pts)
|
||||
flooded_wires = wire_remove # naming compat for the summary print
|
||||
stub_wires = []
|
||||
|
||||
# 5) orphaned power flags (power:*): pin freed by my edit, no kept wire left on it
|
||||
power_spans = []
|
||||
for sy in placed:
|
||||
if sy['ref'] in drop_refs or not sy['lib'].startswith('power:'): continue
|
||||
pcs = sym_pin_coords(t, sy)
|
||||
if pcs and all(pc in freed and pc not in kept_wire_pts for pc in pcs):
|
||||
power_spans.append((sy['s'], sy['e']))
|
||||
|
||||
# 6) dangling no_connects sitting on a removed symbol's pin
|
||||
dangling_nc = [(s, e) for s, e, c, _ in collect(t, 'no_connect') if c in removed_pin_coords]
|
||||
|
||||
# 7) labels orphaned by my edit (any label type): coord freed, no kept wire left,
|
||||
# not already retired. Covers local (label), and cross-sheet (hierarchical_label /
|
||||
# global_label) that only fed a removed connector (e.g. a camera FFC's SCL0/CAM_GPIO).
|
||||
retired_label_spans = {(s, e) for s, e, _ in retired_labels}
|
||||
all_label_objs = collect(t, 'label') + collect(t, 'hierarchical_label') + collect(t, 'global_label')
|
||||
orphan_labels = [(s, e) for s, e, c, _ in all_label_objs
|
||||
if (s, e) not in retired_label_spans and c in freed and c not in kept_wire_pts]
|
||||
|
||||
nc_pins = sorted(nc_pins, key=lambda kv: int(re.sub(r'\D', '', kv[0]) or 0))
|
||||
print(f"sheet: {a.sheet} (module unit {unit}, {len(modpins)} pins)")
|
||||
print(f"symbols to remove: {sorted(found_refs)} (requested {sorted(drop_refs)}; missing {sorted(drop_refs-found_refs)})")
|
||||
print(f"retired-net labels: {len(retired_labels)} | wires removed (anchor-aware): {len(wire_remove)}")
|
||||
print(f"orphaned power flags: {len(power_spans)} | dangling NC removed: {len(dangling_nc)} | orphaned labels: {len(orphan_labels)}")
|
||||
if port_names: print(f"sheet-symbol ports removed: {len(port_spans)} ({sorted(port_names)})")
|
||||
if a.drop_sheet: print(f"whole sheet dropped: {a.drop_sheet} ({len(sheet_spans)} block(s)/instance-path(s))")
|
||||
print(f"module pins to NC ({len(nc_pins)}): " + ", ".join(f"{n}@{xy}" for n, xy in nc_pins))
|
||||
|
||||
# build new text: remove all spans, then append fresh no_connects on exposed module pins
|
||||
spans = sorted(set(sym_spans) | {(s, e) for s, e, _ in retired_labels}
|
||||
| wire_remove_spans | set(power_spans) | set(dangling_nc) | set(orphan_labels)
|
||||
| set(port_spans) | set(sheet_spans), key=lambda x: -x[0])
|
||||
nt = t
|
||||
for s, e in spans:
|
||||
p = s
|
||||
while p > 0 and nt[p-1] in '\t ': p -= 1
|
||||
if p > 0 and nt[p-1] == '\n': p -= 1
|
||||
nt = nt[:p] + nt[e:]
|
||||
# insert no_connect markers before the final closing paren of the sheet
|
||||
nc_block = "".join(
|
||||
f'\t(no_connect\n\t\t(at {xy[0]} {xy[1]})\n\t\t(uuid "{uuidlib.uuid5(uuidlib.NAMESPACE_DNS, a.sheet+n)}")\n\t)\n'
|
||||
for n, xy in nc_pins)
|
||||
last = nt.rstrip().rfind(')')
|
||||
nt = nt[:last] + nc_block + nt[last:]
|
||||
assert nt.count('(') == nt.count(')'), "UNBALANCED — aborting"
|
||||
|
||||
if a.apply:
|
||||
open(a.sheet, 'w').write(nt)
|
||||
print(f"APPLIED. parens balanced ({nt.count('(')}). +{len(nc_pins)} no_connect, -{len(spans)} objects.")
|
||||
else:
|
||||
print(f"DRY RUN ok. parens would balance ({nt.count('(')}). Re-run with --apply to write.")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user