# Etch PCB specs

Etch turns a circuit written as code into a manufacturable PCB. You write the circuit; the service places the
parts, routes the copper, and only returns boards that pass its rule checker. Your job is to produce a correct,
complete spec — not a layout.

## What to produce

Two files:

1. `main.etch` — parts, nets, constraints. Full syntax: `reference/language.md`.
2. `board.toml` — outline size, layer count, edge connectors, keep-outs. Syntax: `reference/language.md` section 6.

Use parts from `reference/parts.md` first. Define a footprint and device in `main.etch` only when the library has
nothing suitable, and take every pad size and position from the part's datasheet (land pattern drawing). Say so in a
comment, and tell the user which dimensions came from where.

## Workflow

1. Ask what the board must do, how it is powered, what connects to it, and any size limit. Do not guess the power
   source or connectors.
2. Pick parts. Prefer library modules (for Wi-Fi/Bluetooth use an `ESP32_*` module — its antenna keep-out is built in).
3. Write `main.etch` with sub-modules for repeated blocks (power, sensors).
4. Write `board.toml`. Start with an outline about 2–3× the total part area; add `[[port]]` for connectors that must
   sit on an edge.
5. The user pastes both files into the Etch checker (free, runs in the browser). If it reports errors, the user pastes
   them back to you. Fix exactly what is reported; do not rewrite unrelated parts.
6. When the checker passes, the user runs the layout on the site.

## Rules that are easy to get wrong

- Every module `port` needs a `net` with the same name inside that module.
- A part with a property block has no `;` after `}`: `part C1: C_0805 { value = 10uF; }`.
- Connect every `GND` pin of a module. On modules the `GND` pin also owns the thermal pad pieces; leaving it
  unconnected fails the checker.
- Never connect `NC…` pins.
- Pin names are exact and case-sensitive (`U1.IO9`, `U1.V3V3`, not `U1.GPIO9` or `U1.3V3`).
- A pin can be on one net only. Two outer nets meeting at one port is an error (it would short them).
- Lengths need units (`5mm`, `8mil`). Currents need `A`/`mA`.
- `constraint current` needs a realistic value. It widens tracks and is checked against the manufacturer's copper data.
- ESP32 boards: add a pull-up resistor plus capacitor on `EN`, a decoupling capacitor near the module's power pin
  (`constraint near`), and keep strapping pins (C3: `IO2`, `IO8`, `IO9`) in their boot-safe state.
- `length` / `match` constraints are only checked, not enforced by the router yet; use them for information, and
  expect a failure if the natural routing does not meet them.
- Coordinates: millimetres with units, origin at the lower-left corner, y up, rotation counter-clockwise.

## Examples

`examples/` holds complete projects that pass the checker and the service's own layout run:

- `examples/ldo/` — 3.3 V LDO power board: J1 (input) -> AMS1117 -> J2 (output) (5 parts, 3 nets)
- `examples/soil_notifier/` — Soil moisture notifier: wakes every 10 minutes, reads a capacitive probe, sends a Wi-Fi notification when dry. (12 parts, 8 nets)

## Library

- `AMS1117_3V3` — pins: `GND`, `VOUT`, `VIN`
- `ESP32_C3_WROOM_02` — pins: `V3V3`, `EN`, `IO4`, `IO5`, `IO6`, `IO7`, `IO8`, `IO9`, `GND`, `IO10`, `IO20_RXD`, `IO21_TXD`, … (18 total)
- `ESP32_C6_MINI_1` — pins: `GND`, `V3V3`, `NC4`, `IO2`, `IO3`, `NC7`, `EN`, `IO4`, `IO5`, `IO0`, `IO1`, `IO6`, … (32 total)
- `ESP32_S3_WROOM_1` — pins: `GND`, `V3V3`, `EN`, `IO4`, `IO5`, `IO6`, `IO7`, `IO15`, `IO16`, `IO17`, `IO18`, `IO8`, … (39 total)
- `ESP32_WROOM_32E` — pins: `VDD`, `EN`, `SENSOR_VP`, `SENSOR_VN`, `IO34`, `IO35`, `IO32`, `IO33`, `IO25`, `IO26`, `IO27`, `IO14`, … (36 total)
- `C_0603` — pads 1, 2
- `C_0805` — pads 1, 2
- `PinHeader_1x02` — pads 1, 2
- `PinHeader_1x03` — pads 1, 2, 3
- `PinHeader_1x04` — pads 1, 2, 3, 4
- `PinHeader_1x05` — pads 1, 2, 3, 4, 5
- `PinHeader_1x06` — pads 1, 2, 3, 4, 5, 6
- `PinHeader_1x07` — pads 1, 2, 3, 4, 5, 6, 7
- `PinHeader_1x08` — pads 1, 2, 3, 4, 5, 6, 7, 8
- `PinHeader_1x09` — pads 1, 2, 3, 4, 5, 6, 7, 8, 9
- `PinHeader_1x10` — pads 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
- `R_0603` — pads 1, 2
- `R_0805` — pads 1, 2
- `SOT_23` — pads 1, 2, 3

Full pin lists: `reference/parts.md`.

---

# Etch language reference

Etch describes a circuit board as code: parts, the copper connections between their pins, and layout rules.
You write two files:

- `main.etch` — the circuit (this document, sections 1–5)
- `board.toml` — the board itself: outline, layer count, fixed parts, keep-out areas (section 6)

The service places and routes the board from these. Every result is checked by a verifier; a board that breaks any
rule is never returned. Anything not described here is rejected, so do not invent syntax.

## 1. Lexical rules

| Element | Rule |
|---|---|
| Comment | `#` to end of line |
| Name | `[A-Za-z_][A-Za-z0-9_]*`, case-sensitive |
| Pad name | a name or an integer: `1`, `A3`, `GND`. Not `3_2` (use `D3_2`) |
| String | `"..."` with only `\"` and `\\` escapes |
| Quantity | decimal number + optional prefix + unit, no space: `0.2mm`, `8mil`, `10k`, `3.3V`, `500mA`, `100nF` |
| Symbols | `{ } ( ) , ; : . = >= <=` |

Quantities: no exponent notation. Prefixes `p n u (or µ) m k M G`. Units `m mil in V A F H Hz ohm W s`.
`mm` = milli-metre, `mA` = milli-ampere. A length must be a whole number of nanometres.

## 2. Footprints — pad shapes and positions

```
footprint R_0603 {
  pad 1 rect(0.8mm, 0.95mm) at (-0.825mm, 0mm);
  pad 2 rect(0.8mm, 0.95mm) at (0.825mm, 0mm);
}
footprint PinHeader_1x02 {
  pad 1 rect(1.7mm, 1.7mm) at (0mm, 0mm) drill 1.0mm;
  pad 2 circle(1.7mm) at (0mm, -2.54mm) drill 1.0mm;
}
```

- Coordinates are relative to the footprint origin, **y points up**.
- Pad shapes: `rect(w, h)`, `circle(d)`, `oval(w, h)` (capsule), `roundrect(w, h, r)` with `0 ≤ 2r ≤ min(w, h)`,
  `poly((x, y), (x, y), …)` (simple polygon, points relative to the pad centre, at least 3).
- `drill d` → through-hole pad on every copper layer. `drill oval(w, h)` → slot. No `drill` → surface-mount pad on the
  outer layer of the side the part is placed on.
- `back` (surface-mount only) → pad on the opposite outer layer.
- `clearance x` → this pad needs at least `x` to copper of other nets (the larger of this and the net/fab value wins).
- `hole at (x, y) drill d;` → non-plated hole (no copper).
- `tie 1, 2;` → net tie: the nets on these pads may join inside this part only.
- `keepout rect(w, h) at (x, y);` → copper keep-out area (rectangle centred at `(x, y)`), moves and rotates with the part.
  No other copper (tracks, vias, pours, other parts' pads) may enter it on any layer; the part's own pads are allowed.
  Use it for antennas. The placer pushes such areas off the board edge when it can.
- Pad names are unique within a footprint.

## 3. Devices — pin names for a footprint

```
device AMS1117_3V3 {
  footprint SOT_223;
  pin GND  = 1;
  pin VOUT = 2, 4;      # one pin may own several pads (tabs, thermal pads) — they are one net
  pin VIN  = 3;
}
```

- A pad belongs to at most one pin. Pads in no pin are not connected (NC).
- All pads of one pin end up on the same net and must be connected by copper.

## 4. Modules — parts and connections

```
module Ldo3V3 {
  port VIN, V3V3, GND;

  part U1: AMS1117_3V3;
  part C1: C_0805 { value = 10uF; voltage = 16V; }
  part C2: C_0805 { value = 22uF; voltage = 10V; }

  net VIN  = U1.VIN, C1.1;
  net V3V3 = U1.VOUT, C2.1;
  net GND  = U1.GND, C1.2, C2.2;

  constraint current V3V3 >= 800mA;
  constraint near U1.VOUT, C2 within 5mm;
}

module Main {
  inst P: Ldo3V3;
  part J1: PinHeader_1x02;
  net VIN = J1.1, P.VIN;
  net GND = J1.2, P.GND;
}
```

| Statement | Meaning |
|---|---|
| `part REF: X;` or `part REF: X { key = value; … }` | One part. With a property block there is **no `;` after `}`**. If `X` is a device, pin names are the device's pins; if `X` is a footprint, pin names are pad names. Properties are recorded only |
| `inst NAME: MODULE;` | Sub-module instance. No recursion |
| `port A, B;` | Nets visible outside the module. Each port needs a `net` statement with the same name inside the module |
| `net NAME = endpoint, …;` | One net. Endpoint is `REF.pin` or `INST.port`. Net names are unique in a module |
| `constraint …;` | Section 5 |

Names of parts, instances and nets must not collide within a module.

### Flattening

1. The top module (set in `board.toml`) is expanded. A sub-part's flat name is `instancePath.REF` (e.g. `P.U1`).
2. `net X = …, P.V3V3` merges the instance's port net `V3V3` into `X`. The merged net takes the outer name.
3. Two different outer names merging into one net is an error (unintended short).
4. A pin can be on only one net.
5. A net with a single endpoint gives a warning (could be a test point).

## 5. Constraints

| Statement | Meaning |
|---|---|
| `constraint width NET >= L;` | Every track of the net is at least `L` wide |
| `constraint clearance NET >= L;` | Copper of the net keeps `L` from other nets (larger of this and fab minimum) |
| `constraint current NET >= I;` | Tracks and vias of the net carry `I` (IPC-2221, conservative; needs fab copper data) |
| `constraint near A, B within L;` | Distance between two points ≤ `L`. Point is `REF.pin` (first pad centre) or `REF` (part origin) |
| `constraint length NET <= L;` | Net length (sum of track centre-lines + vias × board thickness) ≤ `L`. Needs `[stackup]` if the net has vias |
| `constraint match N1, N2, … within L;` | Longest net − shortest net ≤ `L` |

Constraints inside sub-modules apply to the final merged nets.
The router does not lengthen tracks yet, so `length` and `match` can only pass if the natural routing already meets them.

## 6. `board.toml`

```toml
spec = "main.etch"
top = "Main"            # top module
layers = 2              # copper layers
fab = "fab.toml"        # manufacturer rules (in the web app this is chosen for you)
temp_rise_c = 10        # allowed temperature rise for current checks
pour = "GND"            # optional: fill free space with this net (ground plane)

[outline]
rect = ["40mm", "25mm"] # lower-left corner is (0, 0)
# or: polygon = [["0mm","0mm"], ["40mm","0mm"], ["40mm","25mm"], ["0mm","25mm"]]
# cutouts = [[[...], ...]]   tolerance = "0.005mm"

[[fixed]]               # optional: part that must stay at an exact place
ref = "J1"
at = ["2mm", "12.5mm"]
rot = 90                # 0, 90, 180, 270 (counter-clockwise)
side = "top"            # top or bottom

[[hole]]                # optional: mounting hole of the board itself
at = ["3mm", "3mm"]
drill = "3.2mm"

[[port]]                # optional: connector that must sit at an edge
name = "USB"
nets = ["VBUS", "GND"]
part = "J1"
edge = "left"           # left, right, top, bottom
within = "1mm"          # pads at most this far from that edge

[[keepout]]             # optional: fixed copper keep-out area on the board
rect = ["18mm", "0mm", "22mm", "25mm"]   # x0, y0, x1, y1
layers = [1]            # optional; default all layers

[stackup]               # optional, needed for length constraints with vias and for speed scoring
copper_um = [35, 35]
dielectric = [{ thickness = "1.5mm", er = 4.5 }]
```

Unknown keys are rejected (a typo must not silently drop a rule).

## 7. What the verifier checks

Connectivity (every net connected, no shorts, no floating copper), manufacturer rules (track width, clearance,
drill sizes, annular rings, hole spacing, edge clearance), your constraints, current capacity, keep-out areas.
Missing data needed for a check is a failure, not a pass. Not checked yet: signal integrity of high-speed lines,
thermal design, mechanical clearance of part bodies, solder mask, silkscreen.


---

# Etch part library

Use these names directly in `part REF: NAME;`. Devices have named pins; footprints used directly have pad
numbers as pin names. Sizes are pad extents, not body sizes.

## Devices

### AMS1117_3V3
Footprint `SOT_223`, pads span 8.45mm × 5.6mm.

| Pin | Pads |
|---|---|
| `GND` | 1 |
| `VOUT` | 2, 4 |
| `VIN` | 3 |

### ESP32_C3_WROOM_02
Footprint `ESP32_C3_WROOM_02_FP`, pads span 19mm × 12.9mm · antenna keep-out 28mm × 11mm.

| Pin | Pads |
|---|---|
| `V3V3` | 1 |
| `EN` | 2 |
| `IO4` | 3 |
| `IO5` | 4 |
| `IO6` | 5 |
| `IO7` | 6 |
| `IO8` | 7 |
| `IO9` | 8 |
| `GND` | 9, 19 |
| `IO10` | 10 |
| `IO20_RXD` | 11 |
| `IO21_TXD` | 12 |
| `IO18` | 13 |
| `IO19` | 14 |
| `IO3` | 15 |
| `IO2` | 16 |
| `IO1` | 17 |
| `IO0` | 18 |

### ESP32_C6_MINI_1
Footprint `ESP32_C6_MINI_1_FP`, pads span 12.6mm × 10.6mm · antenna keep-out 43.2mm × 20.4mm.

| Pin | Pads |
|---|---|
| `GND` | 1, 2, 11, 14, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, D49_2, D49_3, D49_4, D49_5, D49_6, D49_7, D49_8, D49_9, 50, 51, 52, 53 |
| `V3V3` | 3 |
| `NC4` | 4 |
| `IO2` | 5 |
| `IO3` | 6 |
| `NC7` | 7 |
| `EN` | 8 |
| `IO4` | 9 |
| `IO5` | 10 |
| `IO0` | 12 |
| `IO1` | 13 |
| `IO6` | 15 |
| `IO7` | 16 |
| `IO12` | 17 |
| `IO13` | 18 |
| `IO14` | 19 |
| `IO15` | 20 |
| `NC21` | 21 |
| `IO8` | 22 |
| `IO9` | 23 |
| `IO18` | 24 |
| `IO19` | 25 |
| `IO20` | 26 |
| `IO21` | 27 |
| `IO22` | 28 |
| `IO23` | 29 |
| `RXD0` | 30 |
| `TXD0` | 31 |
| `NC32` | 32 |
| `NC33` | 33 |
| `NC34` | 34 |
| `NC35` | 35 |

### ESP32_S3_WROOM_1
Footprint `ESP32_S3_WROOM_1_FP`, pads span 19mm × 18.96mm · antenna keep-out 48mm × 21mm.

| Pin | Pads |
|---|---|
| `GND` | 1, 40, 41 |
| `V3V3` | 2 |
| `EN` | 3 |
| `IO4` | 4 |
| `IO5` | 5 |
| `IO6` | 6 |
| `IO7` | 7 |
| `IO15` | 8 |
| `IO16` | 9 |
| `IO17` | 10 |
| `IO18` | 11 |
| `IO8` | 12 |
| `USB_DN` | 13 |
| `USB_DP` | 14 |
| `IO3` | 15 |
| `IO46` | 16 |
| `IO9` | 17 |
| `IO10` | 18 |
| `IO11` | 19 |
| `IO12` | 20 |
| `IO13` | 21 |
| `IO14` | 22 |
| `IO21` | 23 |
| `IO47` | 24 |
| `IO48` | 25 |
| `IO45` | 26 |
| `IO0` | 27 |
| `IO35` | 28 |
| `IO36` | 29 |
| `IO37` | 30 |
| `IO38` | 31 |
| `IO39` | 32 |
| `IO40` | 33 |
| `IO41` | 34 |
| `IO42` | 35 |
| `RXD0` | 36 |
| `TXD0` | 37 |
| `IO2` | 38 |
| `IO1` | 39 |

### ESP32_WROOM_32E
Footprint `ESP32_WROOM_32E_FP`, pads span 19mm × 18.96mm · antenna keep-out 48mm × 21.19mm.

| Pin | Pads |
|---|---|
| `VDD` | 2 |
| `EN` | 3 |
| `SENSOR_VP` | 4 |
| `SENSOR_VN` | 5 |
| `IO34` | 6 |
| `IO35` | 7 |
| `IO32` | 8 |
| `IO33` | 9 |
| `IO25` | 10 |
| `IO26` | 11 |
| `IO27` | 12 |
| `IO14` | 13 |
| `IO12` | 14 |
| `IO13` | 16 |
| `NC17` | 17 |
| `NC18` | 18 |
| `NC19` | 19 |
| `NC20` | 20 |
| `NC21` | 21 |
| `NC22` | 22 |
| `IO15` | 23 |
| `IO2` | 24 |
| `IO0` | 25 |
| `IO4` | 26 |
| `IO16` | 27 |
| `IO17` | 28 |
| `IO5` | 29 |
| `IO18` | 30 |
| `IO19` | 31 |
| `NC32` | 32 |
| `IO21` | 33 |
| `RXD0_IO3` | 34 |
| `TXD0_IO1` | 35 |
| `IO22` | 36 |
| `IO23` | 37 |
| `GND` | 1, 15, 38, 39, D39_2, D39_3, D39_4, D39_5, D39_6, D39_7, D39_8, D39_9 |

## Footprints (use pad numbers as pins)

- `C_0603` — 2 pads (1, 2), surface-mount
- `C_0805` — 2 pads (1, 2), surface-mount
- `PinHeader_1x02` — 2 pads (1, 2), through-hole
- `PinHeader_1x03` — 3 pads (1, 2, 3), through-hole
- `PinHeader_1x04` — 4 pads (1, 2, 3, 4), through-hole
- `PinHeader_1x05` — 5 pads (1, 2, 3, 4, 5), through-hole
- `PinHeader_1x06` — 6 pads (1, 2, 3, 4, 5, 6), through-hole
- `PinHeader_1x07` — 7 pads (1, 2, 3, 4, 5, 6, 7), through-hole
- `PinHeader_1x08` — 8 pads (1, 2, 3, 4, 5, 6, 7, 8), through-hole
- `PinHeader_1x09` — 9 pads (1, 2, 3, 4, 5, 6, 7, 8, 9), through-hole
- `PinHeader_1x10` — 10 pads (1, 2, 3, 4, 5, 6, 7, 8, 9, 10), through-hole
- `R_0603` — 2 pads (1, 2), surface-mount
- `R_0805` — 2 pads (1, 2), surface-mount
- `SOT_23` — 3 pads (1, 2, 3), surface-mount


---

# Example: ldo

`main.etch`
```
# 3.3 V LDO power board: J1 (input) -> AMS1117 -> J2 (output)

module Ldo3V3 {
  port VIN, V3V3, GND;

  part U1: AMS1117_3V3;
  part C1: C_0805 { value = 10uF; voltage = 16V; }
  part C2: C_0805 { value = 22uF; voltage = 10V; }

  net VIN  = U1.VIN, C1.1;
  net V3V3 = U1.VOUT, C2.1;
  net GND  = U1.GND, C1.2, C2.2;

  constraint current V3V3 >= 800mA;
  constraint near U1, C2 within 10mm;
}

module Main {
  inst P: Ldo3V3;
  part J1: PinHeader_1x02;
  part J2: PinHeader_1x02;

  net VIN  = J1.1, P.VIN;
  net V3V3 = J2.1, P.V3V3;
  net GND  = J1.2, J2.2, P.GND;
}
```

`board.toml`
```toml
spec = "main.etch"
top = "Main"
layers = 2
fab = "fab.toml"
temp_rise_c = 10

[outline]
rect = ["40mm", "25mm"]
```

---

# Example: soil_notifier

`main.etch`
```
# Soil moisture notifier: wakes every 10 minutes, reads a capacitive probe, sends a Wi-Fi notification when dry.
# Power: 5 V on J1 -> AMS1117 -> 3.3 V. Wi-Fi: ESP32-C3-WROOM-02 module (its antenna keep-out is in the library).

# Parts that are not in the library are defined here. Check sizes against the part's datasheet.
footprint LED_0603 {
  pad 1 rect(0.8mm, 0.95mm) at (-0.825mm, 0mm);   # cathode
  pad 2 rect(0.8mm, 0.95mm) at (0.825mm, 0mm);    # anode
}
device LED_Green_0603 {
  footprint LED_0603;
  pin K = 1;
  pin A = 2;
}
footprint SW_Tactile_6mm {                         # 6 x 6 mm through-hole tact switch, 4 legs
  pad 1 circle(2mm) at (-3.25mm, 2.25mm) drill 1.0mm;
  pad 2 circle(2mm) at (3.25mm, 2.25mm) drill 1.0mm;
  pad 3 circle(2mm) at (-3.25mm, -2.25mm) drill 1.0mm;
  pad 4 circle(2mm) at (3.25mm, -2.25mm) drill 1.0mm;
}
device SW_Tactile {
  footprint SW_Tactile_6mm;
  pin A = 1, 2;      # legs 1-2 are always connected
  pin B = 3, 4;      # legs 3-4 are always connected; pressing joins A and B
}

module Power {
  port VIN, V3V3, GND;
  part U2: AMS1117_3V3;
  part C1: C_0805 { value = 10uF; voltage = 16V; }
  part C2: C_0805 { value = 22uF; voltage = 10V; }
  net VIN  = U2.VIN, C1.1;
  net V3V3 = U2.VOUT, C2.1;
  net GND  = U2.GND, C1.2, C2.2;
  constraint current V3V3 >= 500mA;
  constraint near U2.VOUT, C2 within 5mm;
}

module Main {
  inst P: Power;
  part U1: ESP32_C3_WROOM_02;
  part J1: PinHeader_1x02;            # 5 V input
  part J2: PinHeader_1x03;            # soil probe: 3.3 V, signal, GND
  part SW1: SW_Tactile;               # wake / boot button on IO9 (uses the internal pull-up)
  part D1: LED_Green_0603;
  part R1: R_0603 { value = 1k; }     # LED current limit
  part R2: R_0603 { value = 10k; }    # EN pull-up
  part C3: C_0603 { value = 1uF; }    # EN delay
  part C4: C_0603 { value = 100nF; }  # module decoupling

  net VIN  = J1.1, P.VIN;
  net GND  = J1.2, P.GND, U1.GND, J2.3, SW1.B, D1.K, C3.2, C4.2;
  net V3V3 = P.V3V3, U1.V3V3, J2.1, R2.1, C4.1;
  net EN   = U1.EN, R2.2, C3.1;
  net PROBE = U1.IO4, J2.2;
  net BOOT  = U1.IO9, SW1.A;
  net LED   = U1.IO10, R1.1;
  net LED_A = R1.2, D1.A;

  constraint near U1.V3V3, C4 within 4mm;
  constraint current V3V3 >= 500mA;
}
```

`board.toml`
```toml
spec = "main.etch"
top = "Main"
layers = 2
fab = "fab.toml"
temp_rise_c = 10
pour = "GND"

[outline]
rect = ["50mm", "35mm"]

[[port]]
name = "POWER"
nets = ["VIN", "GND"]
part = "J1"
edge = "left"
within = "3mm"
```
