many eda additions

This commit is contained in:
noise
2026-06-29 16:31:02 -04:00
parent 818f965896
commit 6082613e88
41 changed files with 58827 additions and 1 deletions

View 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).

View 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
View 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

95
PORT_STATUS.md Normal file
View File

@@ -0,0 +1,95 @@
# 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).
## 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), ERC 182 → 136, 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 |
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** (NVMe M-key — not used; the M.2 **E-key** Wi-Fi will be a
FRESH sheet in the add phase, per design decision). Use `--sheet-ports` for its root hierarchical pins.
2. **Re-baseline**: capture a new analyzer baseline of `carrier/` so the add phase diffs against the
stripped design, not the original reference.
3. **Add phase**: 15V→5V buck front-end (reverse-protection + TVS), M.2 E-key (PCIe Gen2 ×1 +
CLKREQ/PERST/RF_KILL + 3.3V), GPS connector, PPS distribution (GPS PPS → TVS → Schmitt buffer →
fan-out: CM5 `Ethernet_SYNC_OUT` + 2 header taps). SPICE-validate the buck divider + PPS network.
## 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.

View 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"
}

View 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

View 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.

View File

@@ -0,0 +1 @@
z_min=0.00776885 f_at_zmin= z_at_1M=0.0092502 z_at_100k=0.0781266 n_caps=6

View 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

View 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

View 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

View 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

View 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.

View File

@@ -0,0 +1 @@
z_min=0.00752546 f_at_zmin= z_at_1M=0.00898326 z_at_100k=0.0569392 n_caps=3

View 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

View 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.

View File

@@ -0,0 +1 @@
i_peak= t_peak= v_settled=5 v_target=5.0 r_out=0.1

View 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

View 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.

View File

@@ -0,0 +1 @@
i_peak= t_peak= v_settled=3.327 v_target=3.327 r_out=0.01

View 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

View 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 ===

View File

@@ -0,0 +1 @@
vfb_sim=0.599951 error_pct=7.27272E-05 expected=0.599951256 vin=3.327 vref=0.6

View 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

View 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 ===

View File

@@ -0,0 +1 @@
vout_sim=0.595082 error_pct=7.27272E-05 expected=0.5950823999999999

844
carrier/CM5IO.kicad_pro Normal file
View File

@@ -0,0 +1,844 @@
{
"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|1524000|317500|2cecda96-8fc5-40d2-a9f4-ae1444adbd06|00000000-0000-0000-0000-000000000000|/e63e39d7-6ac0-4ffd-8aa3-1841a4541b55/00000000-0000-0000-0000-00005ed4bb5b|/e63e39d7-6ac0-4ffd-8aa3-1841a4541b55/00000000-0000-0000-0000-00005ed4bb5b|",
"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)"
],
[
"00000000-0000-0000-0000-00005ed4bb5b",
"PCIe-M2"
]
],
"text_variables": {}
}

3816
carrier/CM5IO.kicad_sch Normal file

File diff suppressed because it is too large Load Diff

10220
carrier/CM5IO.kicad_sym Normal file

File diff suppressed because it is too large Load Diff

19656
carrier/CM5_GPIO.kicad_sch Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

7933
carrier/PCIe-M2.kicad_sch Normal file

File diff suppressed because it is too large Load Diff

4
carrier/fp-lib-table Normal file
View 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
View File

@@ -0,0 +1,4 @@
(sym_lib_table
(version 7)
(lib (name "CM5IO")(type "KiCad")(uri "${KIPRJMOD}/CM5IO.kicad_sym")(options "")(descr ""))
)

File diff suppressed because it is too large Load Diff

View 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

64
tools/install-kicad-toolkit Executable file
View 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
View 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
View 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" "$@"

266
tools/retire_block.py Normal file
View File

@@ -0,0 +1,266 @@
#!/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 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('--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())
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)})")
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), 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()