Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions HANDOVER.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ manager, CAN 2.0B @500k. Docs 01–07 are the spec; read 05 (build plan) first
| PCB — Phase 2 | **83 unconnected, 0 copper DRC, silk clean (2026-07-26, c1a86eb)** — see the resume section; finish by hand in KiCad. Older detail below: |
| PCB — Phase 2 (history) | **(2026-07-25, 27fb2d1).** `gen_board.py` placement green (173 comps, all pour/courtyard/edge assertions pass, 130×90 4-layer); `route_board.py` pass-1 done (power pours, In2 heat patches, both phases' gate fan-outs, Kelvin pairs, disconnect trunk). DRC: **copper down to ~19 clearance + ~11 dangling/mask/hole in 3 known clusters** (see resume section); 243 unconnected = signal nets, autoroute not yet run. Found & fixed a real LM5143 land-pattern bug in the process (see load-bearing decisions) |
| PCB — Phase 3 backplane | **COMPLETE pass-1 (2026-07-25)**: `phase3-backplane/tools/gen_board.py` (single-script: placement + 2oz bus pours + stitching + ALL signal routing) — **0 copper DRC, 0 unconnected**; only lib-bookkeeping (39) + 1 silk nick remain. 8 slots @30mm (XT60PW-F rot-90 mates the module pad-for-pad, socket y60..77.8 = module J5 1:1), M6 lugs -> RS1‖RS2 0.5mΩ Kelvin-sensed by INA228, nested PRESENT L-bus, CAN terminated past both end slots, E-stop chain threaded per docs. Netlist from `tools/wip/bp.net` (regenerate via kicad-cli) |
| PCB — Phase 3 manager | **placement pass complete, 0 DRC (2026-07-26)**: 100×80 2L, 87 footprints (80 comps + 4 M3 + 3 fiducials), 86 nets, F.Cu 3V3 / B.Cu PGND planes, antenna keep-out verified copper-free. **173 unconnected = the signal nets; routing is the next pass.** Reproduce: `cd tools && python3 gen_board.py wip/mgr.net` → `python3 ../../common/fix_fpids.py ../phase3-manager.kicad_pcb` → `python3 ../../common/finish_board.py ../phase3-manager.kicad_pcb --silk --planes` → `python3 run_drc.py` |
| PCB — Phase 3 manager | **Routed, 0 unconnected, 0 DRC errors (2026-09-27, draft PR into `development` from `claude/route-manager-shxsg1`)**: placement reworked (15+ passives had been placed by stale refdes, far from their pins; U8 buck re-laid tight round its pinout; U11 turned so CAN faces J1; U12 in the USB path), PGND now poured on both layers (was F.Cu 3V3 / B.Cu PGND), USB + CAN pairs and buck power copper hand-drawn and locked (`route_critical.py`), rest Freerouting 1.9 + `finish_routes.py`, 283 PGND stitch vias. DRC = 5 warnings (4 silk, 1 U8 courtyard-override lib mismatch). Antenna keep-out copper-free. Pipeline: `phase3-manager/tools/README.md` |
| Module firmware | v0.1 builds clean (6.3 KB): full peripheral binding + CAN dispatch around the host-tested `module_core`. Untested on silicon (no board yet) |
| Host tests | `cd firmware/tests && make test` — must stay green. **5 suites now**: can, core, manager, scpi, ui |
| Manager firmware | **v0.2 COMPLETE (2026-07-25, commit 174595e)**: `scpi_core` + `ui_core` join `manager_core` as host-tested cores; ESP-IDF shell fully written (display/encoder/USB-SCPI/app_main). **First compile 2026-09-27: builds clean on IDF v5.3.2** (355 KB image, two build fixes). Untested on silicon. See docs/10 |
Expand Down Expand Up @@ -198,10 +198,10 @@ Notable route_board facts a future session needs:
1. **Finish Phase-2 board** — hand-route the remaining 83 connections in
KiCad (table + per-item coords in the resume section), then
`../common/finish_board.py` (silk + planes + fab) and `run_drc.py`.
2. **Phase-3 manager board — ROUTE it.** Placement is done and DRC-clean
(see the table); what is left is 173 unconnected signal nets on a 2-layer
board. Mostly 3-node digital nets, so hand-routing in KiCad is realistic;
there is no `route_board.py` for this phase yet.
2. **Phase-3 manager board — routed (2026-09-27).** Review before ordering:
the J3 breakout is tied to the USB4105 land pattern while the USB-C part
is still an open pick; 13 nets cross under the USB pair on B.Cu (fine at
full speed); the 0.2 mm drilling gate below still applies.
3. **Order files** — the MPN/LCSC pass is done for every part SOURCING.md
verified: `python3 common/bom.py` (from `hardware/`) writes hidden
LCSC/MPN/Manufacturer properties into the schematics (surgical, additive
Expand Down
9 changes: 7 additions & 2 deletions hardware/LAYOUT.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ against the footprints + netlist, not memory.
| phase1-module | done | green | **done** — `phase1-module.kicad_pcb`: placement, pours, planes, critical routes, 0 DRC copper errors; some signal nets unrouted (autoroute.py is WIP) |
| phase2-module | done (hand-arranged) | green (173 comps, 116 nets) | not started — this doc is the placement plan |
| phase3-backplane | done | green (30 comps) | not started |
| phase3-manager | done | not yet re-checked | not started |
| phase3-manager | done | green (80 comps, 86 nets) | **routed** 2026-09-27: 0 unconnected, 0 DRC errors (see `phase3-manager/tools/README.md`) |

Phase-1's board is the reference implementation: open it next to this doc —
every rule below is applied there and visible (input band → FET straddle →
Expand Down Expand Up @@ -264,14 +264,19 @@ with the bottom row facing the FETs, and give the top edge an **AGND pocket**
- Keep the manager connector and any supply-sense divider at the same end as
the supply entry.

# Phase-3 manager (100 × 80 mm, 2-layer) — preliminary
# Phase-3 manager (100 × 80 mm, 2-layer) — routed 2026-09-27; the rules it follows

- ESP32-S3 module: antenna edge overhanging or keep-out (no copper under the
antenna), away from the display/CAN wiring.
- Standard rules: 100n at every supply pin pair + bulk at the module; CAN
transceiver next to the backplane connector, differential pair, termination
per the bus plan above; USB connector ESD diodes at the connector; buttons/
encoder and display connector grouped on the UI edge (see ui.kicad_sch).
- As built: PGND poured on both layers and stitched (a 2-layer board's top
layer carries the signals, so a top-side 3V3 plane only fragments); B.Cu
kept for short jumpers so it stays the reference plane under the USB and
CAN pairs; each LMR36015 VIN/PGND pin pair has its own input cap at the
pins; U12 (ESD) sits in the USB pair's path, not on a stub.

---

Expand Down
19 changes: 15 additions & 4 deletions hardware/phase1-module/tools/finish_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@
VIA_COST = 1.5
ZONE_COST = 0.6 # per cell through a foreign F.Cu pour fill
RIP_COST = 0.4 # per 0.05 mm cell through rippable copper
# Boards that reuse this router (phase3-manager/tools/finish_routes.py)
# override these; the defaults keep phase-1 behaviour unchanged.
FILL_AROUND = set() # nets whose F.Cu pour is plain fill-around: no ZONE_COST
B_COST = 0.0 # extra cost per cell on B.Cu (keeps a B.Cu plane whole)
COPPER = (pcbnew.F_Cu, pcbnew.In1_Cu, pcbnew.In2_Cu, pcbnew.B_Cu)
ROUTE_LAYERS = (pcbnew.F_Cu, pcbnew.B_Cu)
mm = pcbnew.ToMM
Expand Down Expand Up @@ -117,6 +121,8 @@ def __init__(self, board, base_keys=None):
obj = None if key(t) in base_keys else t
self.items.append((net, t.GetLayer(), g, "track", obj))
for z in board.Zones():
if z.GetIsRuleArea():
continue # keep-outs have no fill (asserts in KiCad 7)
for g in sps_polys(z.GetFilledPolysList(z.GetLayer())):
self.items.append((z.GetNetname(), z.GetLayer(), g, "zone", None))

Expand Down Expand Up @@ -196,9 +202,9 @@ def soft(it):
# refills around the track), so a pin walled in by a pour can still
# get out -- but the router minimises the cut
pour = mask([it[2] for it in items if it[1] == pcbnew.F_Cu and it[0] != net
and it[3] == "zone"], CLEAR + TRACK_W / 2)
and it[3] == "zone" and it[0] not in FILL_AROUND], CLEAR + TRACK_W / 2)
pour_via = mask([it[2] for it in items if it[1] == pcbnew.F_Cu and it[0] != net
and it[3] == "zone"], CLEAR + VIA_D / 2)
and it[3] == "zone" and it[0] not in FILL_AROUND], CLEAR + VIA_D / 2)
softg = {L: [it[2] for it in items if it[1] == L and it[0] != net and soft(it)]
for L in ROUTE_LAYERS}
track_ok = {L: ~mask(foreign[L], CLEAR + TRACK_W / 2) for L in ROUTE_LAYERS}
Expand Down Expand Up @@ -287,7 +293,8 @@ def h(i, j):
if di and dj and not (ok[l][i + di, j] and ok[l][i, j + dj]):
continue # no corner cutting past an obstacle
nc = c + w * STEP + (RIP_COST if sm[l][a, b] else 0.0) \
+ (ZONE_COST if l == 0 and pour[a, b] else 0.0)
+ (ZONE_COST if l == 0 and pour[a, b] else 0.0) \
+ (B_COST if l == 1 else 0.0)
ns = (l, a, b)
if nc < best.get(ns, 1e18):
best[ns] = nc
Expand Down Expand Up @@ -509,10 +516,14 @@ def main():
args = [a for a in sys.argv[1:] if not a.startswith("--base=")]
base = [a[7:] for a in sys.argv[1:] if a.startswith("--base=")]
board_path = args[0] if args else BOARD
board = pcbnew.LoadBoard(board_path)
# Load the base BEFORE the board: pcbnew.LoadBoard switches the active
# project, and a base outside the project dir has none -- loaded second,
# it swapped the board's rules for KiCad defaults, which Save() then
# wrote into the real .kicad_pro (min drill 0.3, hole clearance 0.25...).
base_keys = set()
if base:
base_keys = {key(t) for t in pcbnew.LoadBoard(base[0]).GetTracks()}
board = pcbnew.LoadBoard(board_path)
pcbnew.ZONE_FILLER(board).Fill(board.Zones())
bb = board.GetBoardEdgesBoundingBox()
board_bb = (mm(bb.GetLeft()), mm(bb.GetTop()), mm(bb.GetRight()), mm(bb.GetBottom()))
Expand Down
Loading
Loading