Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ flash_data/
*-backups/
managed_components/
dependencies.lock
__pycache__
281 changes: 252 additions & 29 deletions ecad/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,62 +12,285 @@ The ESP-BOX-EMU contains a single circuit board with the following components:
- AW9523 I/O expander
- MCP73831 LiPo charger
- MAX17048 LiPo state-of-charge monitor (I2C)
- MicoSD card slot
- TPS61070 boost converter
- MicroSD card slot
- TPS63020 5V buck-boost converter (battery -> 5V for the box)

These features are supported in two different versions of the electronics, targeting:
- ESP32-S3-Box
- ESP32-S3-Box-3

## Layout of this folder

- `elec/src/` - atopile sources (modern atopile >= 0.15 syntax)
- `elec/src/parts/` - atomic parts (auto-generated from LCSC plus local
custom parts for the GBC membranes/buttons, board outlines and edge fingers)
- `elec/layout/` - KiCad layouts, one folder per build (regenerated by
`ato build`; the pre-2024 routed layouts live in `elec/layout-legacy/`)
- `elec/footprints-legacy/` - the old shared footprint library. Modern
atopile does not use this; the footprints were copied into the atomic
parts under `elec/src/parts/`. Kept for reference.
- `attic/` - unmigrated atopile 0.2-era sources kept for reference
- `Box Electronics v*.sch/brd`, `Box-3 Electronics v*.sch/brd` - the
Fusion 360 / Eagle design the boards were actually manufactured from

## Setup

``` sh
# one time steps on your machine
pipx install atopile
ato configure

# one time steps in this folder
ato install
# one time steps on your machine (any of these; uv shown)
uv tool install --python 3.14 atopile
```

Dependencies (e.g. `atopile/ti-tps63020`) are fetched automatically into
`.ato/modules/` on first build; `ato add <package>` adds new ones.

## Build

``` sh
# build everything
ato build -t all
ato build

# build for ESP32-S3-BOX
ato build -b box-emu -t all
ato build -b box-emu

# build for ESP32-S3-BOX-3
ato build -b box-3-emu -t all
ato build -b box-3-emu
```

Note: if you make changes to the electronics design (.ato files), you'll likely
need to update the `box-emu-base` layout (or other affected layouts). After
updating the affected layouts, make sure you import them into their parent
modules. The tree is listed here:
## Ordering / manufacturing notes

* box-3-emu
* box-3-connector
* box-emu-base
* gbc-dpad
* gbc-start-select
* gbc-a-b-x-y
* gbc-a-b
* box-emu
* box-connector
* box-emu-base
**Surface finish: order ENIG, not HASL.** The GBC button footprints expose
bare-copper interdigitated fingers that the carbon membrane pucks press
against. With HASL those fingers come back coated in solder (uneven, prone
to oxidation) and the buttons work poorly; with ENIG they come back flat
gold, which is the standard finish for carbon-contact keypads. This is a
per-order fab option (JLCPCB: "Surface Finish" -> "ENIG") and cannot be
enforced from the design files.

Related, already handled in the footprints: the buttons' four corner
connection pads are tented (copper-only, no paste, no mask opening), so
PCBA assembly applies no solder anywhere on the button contact areas. If
you regenerate or edit `GBC_*_BUTTON` footprints, keep the pads
`(layers "F.Cu")` only.

## Working on the KiCad layouts

Each build owns one board file: `elec/layout/<build>/<build>.kicad_pcb`
(KiCad 9+ format; there is no `.kicad_pro` — open the `.kicad_pcb` directly
in KiCad's PCB Editor). The `fp-lib-table` next to each board resolves all
footprints from the atomic parts in `elec/src/parts/`, so the boards are
self-contained as long as the repo layout is intact.

The `.ato` sources are the source of truth for everything electrical;
KiCad is only for placement and routing:

``` sh
# build a target and open its layout in KiCad in one step
ato build -b box-3-emu --open

# or open it manually
open "elec/layout/box-3-emu/box-3-emu.kicad_pcb"
```

The edit loop:

1. Change schematics by editing the `.ato` files (never add/remove
components or change connectivity in KiCad — the next build will
revert it).
2. Run `ato build -b <build>`. This syncs footprints and nets into the
board file. Footprints are matched by their `atopile_address` property,
so your existing placement and routing are preserved; new components
appear stacked near the origin for you to place.
3. Place/route in KiCad and save. **Close the board (or at least save it)
before the next `ato build`** — atopile rewrites the file on disk, and
KiCad's PCB editor will not auto-reload; reopen the file after each
build. A stale `~<name>.kicad_pcb.lck` file means KiCad still has it
open.

Useful flags when iterating on layout: `--keep-designators`,
`--keep-net-names`, and `--keep-picked-parts` take those from the existing
PCB instead of regenerating them, and `--frozen` fails the build if the
PCB would change at all (good for CI).

### Layout reuse (shared button clusters)

Every module instance becomes a KiCad *group* in the board, and the child
builds (`gbc-a-b`, `gbc-dpad`, ...) exist so their layouts can be reused by
the parents. When a parent build adds a sub-module group whose footprints
are all new, atopile pulls that group's placement from the child build's
board. So do the layouts bottom-up, running `ato build` between levels:

* box-3-emu / box-emu (top-level: connector position, final routing)
* box-3-connector / box-connector
* box-emu-base (most of the placement/routing work)
* gbc-dpad
* gbc-start-select
* gbc-a-b-x-y
* gbc-a-b

i.e. lay out `gbc-a-b` first, then `gbc-a-b-x-y` (which pulls the a-b
group), then the other button boards, then `box-emu-base`, and finally the
two top-level variants. The automatic pull only happens when the group
first appears in the parent, so later tweaks to a child layout are not
force-propagated over a parent you've already edited.

To re-pull an updated child layout into a parent on demand, either use the
"Pull Group" toolbar button in the PCB editor (select the group first;
atopile installs the plugin into `~/Documents/KiCad/<ver>/scripting/plugins/`
-- as of atopile 0.15.x only up to KiCad 9.0, so for KiCad 10 copy the
`atopile.py` loader from the `9.0` folder into `10.0` and Tools > External
Plugins > Refresh), or run the same thing from the CLI:

``` sh
ato kicad-ipc layout-sync --legacy \
--board elec/layout/box-3-emu/box-3-emu.kicad_pcb \
--include-group connector # omit to just sync group membership
```

### Shared base layout across the two variants

`BoxEmuBase` is one module, so it is one group (`box`) in both top-level
boards, and its layout — placement *and* routing — lives in exactly one
place: `elec/layout/box-emu-base/box-emu-base.kicad_pcb`. The intended
workflow:

## View
1. Place and route everything except the connector in the **box-emu-base**
build (use `positions.json` + the placement script below for the
mechanically-constrained parts, then route by hand).
2. Push it into both variants:

``` sh
./scripts/sync-base-layout.sh # pulls groups "box" and "connector"
# into box-emu and box-3-emu, then snaps
# the connector group to the base group
# so the two outline pieces close exactly
# (scripts/align-base-connector.py)
```

3. In `box-emu` / `box-3-emu`, only route the connector-bound signals --
group placement and outline closure are handled by the sync script.
The relative outline offsets were calibrated by fitting all outline
footprints against the Eagle board outlines (0.000 mm rms) and live in
`scripts/align-base-connector.py`.

The connector footprints themselves (TE dock connector on box-3, PMOD /
power edge fingers on box) are placed within their boards from the STEP
assemblies via `elec/layout/box-connector/cad_map.json` and
`elec/layout/box-3-connector/cad_map.json`, same flow as the base board
(export -> place-parts apply -> build). Note the box-3 dock connector's
r=180 is meaningful: it puts pad A1 on the side that mates with the box,
matching the manufactured Eagle board.

Pulls replace the contents of the pulled group, so keep variant-specific
routing (base-to-connector tracks) outside the `box` group — tracks you
draw in the parent are not group members by default, so this is the
natural behavior; just don't add them to the group.

### Scripted placement from MCAD

Mechanically-constrained parts (buttons, membranes, USB-C, uSD, battery
connector, volume switches) have known positions that come from the
enclosure CAD. Those live in `elec/layout/box-emu-base/positions.json`,
keyed by each footprint's stable `atopile_address` (e.g.
`dpad.up_button.btn`, `usb_c.conn`, `uSD.card`) so they survive rebuilds
and designator changes. When the MCAD/STEP changes, update the numbers and
re-apply:

``` sh
# apply positions to the board (position, rotation, front/back side)
python3 scripts/place-parts.py apply \
elec/layout/box-emu-base/box-emu-base.kicad_pcb \
elec/layout/box-emu-base/positions.json

ato build -b box-emu-base # re-sync the board
./scripts/sync-base-layout.sh # push into both variants
```

`scripts/place-parts.py dump <board>` regenerates the JSON from the
board's current state (useful to bootstrap the file or to capture manual
edits back into it). The optional `transform` block (`dx`/`dy`/`flip_y`)
converts CAD coordinates into KiCad's frame (KiCad y points down). Parts
not listed in the JSON are never touched.

### Generating positions.json from the Fusion 360 assembly

Instead of typing coordinates by hand, positions can be extracted from a
STEP export of the enclosure assembly (the PCB must be present as a
component in the assembly). Export the full assembly from Fusion 360 as
STEP (e.g. to `mcad/esp-box-emu-assembly.step`), then:

``` sh
# 1. discover occurrence names in the STEP
uv run scripts/export-positions-from-step.py list mcad/esp-box-emu-assembly.step

# 2. fill in elec/layout/box-emu-base/cad_map.json:
# - board.occurrence: the PCB component's name
# - board.kicad_origin / flip_y: the CAD point that is KiCad (0,0)
# - each part's "cad" name (+ one-time r_offset rotation calibration)

# 3. extract placements into positions.json
uv run scripts/export-positions-from-step.py export \
mcad/esp-box-emu-assembly.step \
elec/layout/box-emu-base/cad_map.json \
elec/layout/box-emu-base/positions.json

# 4. apply / rebuild / propagate as usual
python3 scripts/place-parts.py apply \
elec/layout/box-emu-base/box-emu-base.kicad_pcb \
elec/layout/box-emu-base/positions.json
ato build -b box-emu-base
./scripts/sync-base-layout.sh
```

The script is self-contained (`uv run` fetches its OCP dependency on first
use). Front/back side is detected from the part's frame orientation and
can be overridden per entry with `"side"`; `"r_offset"` absorbs the
difference between footprint and CAD 0-degree orientations; `"offset"`
(anchor-frame mm) places bodiless parts relative to a CAD neighbor (used
for the a/b/x/y/start/select carbon pads, anchored to their membranes).
Re-run steps 3-4 whenever the MCAD changes.

The current `cad_map.json` is fully calibrated against `mcad/esp-box-emu.step`
(2026-07): the Fusion board component uses the Eagle brd coordinate frame
verbatim, and the BOX_EMU_BASE outline matches the Eagle outline exactly, so
`kicad_origin` is exact (0.000 mm rms over 54 matched components + 7 outline
vertices). One caveat: positions for USB-C, uSD, battery JST, motor header
and the volume switches are the *old* board parts' anchor points; the new
LCSC parts may use slightly different footprint anchor conventions, so
eyeball those five against the enclosure once and absorb any difference into
their `"offset"` in cad_map.json.

### KiCad 10 file-format warning

atopile 0.15.x reads/writes the KiCad 9 board format (`version 20241229`).
**KiCad 10 silently upgrades any board it saves to `version 20260206`,
which atopile cannot parse** -- builds of that target fail and layout
pulls silently skip the group ("Group ... not found in layout maps").
Until atopile ships KiCad 10 support, after editing a board in KiCad 10
run:

``` sh
ato view -b box-emu
# or
ato view -b box-3-emu
python3 scripts/downgrade-kicad10-pcb.py elec/layout/<build>/<build>.kicad_pcb
ato build -b <build> # normalizes the file back to atopile's format
```

(or do layout work in KiCad 9, which reads and writes the format natively).

Note: the boards in `elec/layout/` were regenerated during the atopile
0.15 migration, so placement/routing must be redone by hand. The old
routed boards (atopile 0.2 era, KiCad 6 format) are preserved in
`elec/layout-legacy/` — they can be opened in KiCad side-by-side as a
placement reference, but their components can't be auto-matched to the
new builds.

## Differences from the manufactured (Fusion 360) boards

The atopile design intentionally deviates from the Eagle/Fusion `v36`/`v11`
boards in a few places, mostly to reduce battery drain when the board sits
idle (see git history for details):

- battery sense divider is 1M + 1M with a 100nF sampling cap (was 10k + 10k,
which drained ~200uA from the battery continuously)
- 5V rail comes from a TPS63020 buck-boost in power-save (PFM) mode with
VINA/EN properly tied to VIN and PS/SYNC tied low
- DRV2605L gets its datasheet-required 1uF REG bypass and a VDD decoupler
- the AW9523 interrupt line is routed to the box (IO21) on both variants
79 changes: 0 additions & 79 deletions ecad/ato-lock.yaml

This file was deleted.

Loading
Loading