pixalynx/nrf9151-gps-tracker
A compact 44×32 mm, 4-layer nRF9151 LTE-M/NB-IoT/GNSS tracker PCB with nPM1300 LiPo/USB-C power, nano-SIM/eSIM switching, nRF7002 Wi‑Fi scanning, accelerometer, antennas, buzzer, LEDs, SOS button, and SWD/debug connectors.
- Version
- 0.1.4
- License
- unset
- Stars
- 0
docs/design.md
# nRF9151 GPS tracker — design report (Rev A, 23 September 2026)
A small rechargeable LTE-M/NB-IoT + GNSS + Wi-Fi-positioning tracker built around the Nordic **nRF9151** SiP. It is meant to go in a child's bag or pocket, or on a pet collar. The nRF9151 application core is the only processor.
```
USB-C (power only) ─ VBUS ─┐ ┌─ FB1 ─ nRF9151 VDD (3.0-5.5 V)
├─ nPM1300 ─ VSYS ──────────────┼─ RT9080 3.3 V LDO (EN = P0.02) ─ nRF7002 VBAT, buzzer, LS1
JST-PH LiPo ── VBAT ───────┘ │ BUCK1 1.8 V ─ VDD_GPIO, LIS2DW12, LNA, SIM switch, TWI, LS2 -> nRF7002 IOVDD
RT1 10k NTC ─ NTC │ LED1 (charge) / LED2 (status) sinks, SHPHLD = SOS button, GPIO0 = interrupt
nRF9151 ── ANT ── pi-match (DNP) ── J1 U.FL (LTE flex antenna)
── GPS ── SKY65943-11 LNA/SAW ── J2 U.FL (GNSS patch/flex; bias-T for an active antenna, DNP)
── SIM port ── NX3DV2567 ──(S=L)── TPD3F303 ── nano-SIM socket (J4)
└(S=H)── MFF2 eSIM (U8, provider-supplied)
── SPI ── nRF7002 ── TXRF0/TXRF1 ── diplexer ── J3 U.FL (2.4/5 GHz flex antenna)
── TWI ── nPM1300, LIS2DW12 (INT1/INT2 wake)
── PWM ── DMG1012T ── MLT-5020 buzzer
── SWD ── Tag-Connect TC2030-NL pads (J7)
```
## Decisions
See [decisions.md](decisions.md) for what was agreed up front: USB-C charging, U.FL antennas, all four features, and nano-SIM + switchable eSIM. It also lists the calls made during the design.
| Block | Choice | Basis |
| --- | --- | --- |
| Modem / MCU | nRF9151-LACA-R7 (C22397843), Nordic reference VDD filter: C1/C2 1 µF + C3 47 µF on VSYS, 22 Ω BLM18SD bead, C4 10 µF (10 V 0402) at the VDD pin, ENABLE→VDD 10 kΩ, DEC0 exactly 4.7 µF, nRESET 1 kΩ + 100 nF, COEX0 100 kΩ pull-down | nRF9151 PS / HDG. The block is the one checked on pixal-gps, `lib/layout.ts` `rel()`. |
| LTE | U.FL J1, shunt–series–shunt reservation (series 0 Ω fitted, shunts DNP) | 50 Ω connector and cable need no match. The pads are kept for tuning. |
| GNSS | SKY65943-11 LNA with pre/post SAW, 9.1 nH inter-stage, enabled by COEX0, bias-T (68 nH, DNP) from PMIC load switch 1 for an active antenna | nRF9151 DK / Thingy:91 X front end |
| Wi-Fi locating | nRF7002-QFAA (C5370744; nRF7000 is pin-compatible), Nordic reference circuit C1–C18 → C41–C58, unused pins per Rev 1 Errata [9] (2–5, 39–42, 45 to GND; SPI pull-downs in the host), 3.3 µH buck inductor, 40 MHz 8 pF crystal with no load caps, TDK DPX165950DT diplexer, U.FL J3 | nRF7002 PS v1.2 §14.3, rechecked against PS v1.3 (see research/nrf7002.md) |
| Power | nPM1300: charger, power path, BUCK1 1.8 V (VSET1 47 kΩ), BUCK2 disabled (config 3), LS1 → VANT, LS2 → nRF7002 IOVDD, LED sinks, SHPHLD button, fuel-gauge ADC | nPM1300 PS / HDG |
| 3.3 V rail | RT9080-33 LDO (C841192, 2 µA Iq, 600 mA, EN from P0.02) from VSYS | The nRF7002 needs 2.9–4.5 V at up to 260 mA. VSYS reaches 5.5 V on USB and BUCK2 tops out at 200 mA, so the LDO covers both limits. |
| SIM | NX3DV2567 4PDT (0.5 Ω supply pole for VCC), S low = nano-SIM (default, 100 kΩ pull-down), S high = MFF2 eSIM; TPD3F303 ESD/EMI filter on the socket side only | nRF9151 DK sheet 7 topology |
| Motion | LIS2DW12 at 0x18, INT1/INT2 → P0.10/P0.11 | ~1 µA wake-on-motion |
| UI | Side-actuated SOS button on SHPHLD; red (charge) + green (status) 0402 LEDs on the PMIC sinks; MLT-5020 5 mm buzzer, 4 kHz PWM via DMG1012T with RB521S30 flyback | |
| Battery | JST-PH 2.0 S2B-PH-SM4-TB, pin 1 = BAT+; on-board 10 kΩ NTC for JEITA | SparkFun convention; **check each cell's polarity** |
| Debug | Tag-Connect TC2030-NL (SWD + nRESET + VREF), logs over RTT | |
| Board | 44 × 32 mm, 4 layers (JLC04161H-7628, 1.6 mm), L2 solid GND, 0.2/0.4 mm through vias, 0.1 mm track / 0.105 mm space, supply path 0.3–0.4 mm, double-sided assembly | |
## Placement
**Top side**
- **Left:** the nRF9151 with its decoupling column, LTE chain to J1 at the bottom, GNSS LNA to J2 below-centre.
- **Top-left:** buzzer and driver, LIS2DW12.
- **Top edge:** SOS button and the two LEDs.
- **Centre-right:** the nRF7002, rotated so its RF pins face the diplexer and J3, the SPI pins face the nRF9151, and the XO/LDO pins face open space for the 0201 capacitors. Its buck side faces left.
- **Right edge:** USB-C (bottom-right) and the JST (top-right).
**Bottom side**
- nano-SIM socket under the SiP, card entering from the left edge, with the TPD3F303, NX3DV2567 and MFF2 eSIM below it.
- nPM1300 and its passives between the USB-C and the JST. The input capacitors C20/C22/C23 sit in a column at the board edge facing the VBAT/VBUS/VBUSOUT pins. C21 (VSYS) sits under VOUT2.
- The RT9080 LDO, the 40 MHz crystal (under the nRF7002's XO pins, one via per line), the nRF7002's VBAT/DIGVDD/PWRIOVDD capacitors, and the NTC.
## Routing
The board is fully routed: 80/80 nets joined through copper, 0 tscircuit errors, 0 clearance findings at 0.1 mm, 94/94 design checks (`bun run check:routed`).
How it is built (`bun run route:all`, then `bun run route:widen` and `bun run check:routed`):
1. **Fixed copper in `index.circuit.tsx`**, laid before the router:
- the RF lines, the crystal, and the exposed-pad ties;
- the nPM1300 escapes: I2C fan-outs on the top row, staggered load-switch vias on the bottom row, and the right-hand corner (VBAT, VSYS, VBUS, VBUSOUT into C20/C22/C23);
- the 0.4 mm battery feed from the JST on top.
2. **Ground mesh** (`gen-gnd-mesh.py`): a stub and via into the inner1 plane for every ground land.
3. **Freerouting 1.9** through a Specctra DSN (`export-router-dsn.py`):
- signals at 0.1 mm with 0.105 mm clearance;
- VSYS, VBAT and VDD_NRF in a 0.3 mm class; the other rails at 0.15 mm;
- the RF corridors and the board edge are keepouts.
4. **Import** (`import-router-session.py`): the router's wire fragments are stitched into land-to-land legs (`routing/index-routes.json`), including legs that end on fixed copper the router joined. `fix-anchors.py` corrects the J5/J6 frames. `widen-power-legs.py` widens the rails where clearance allows.
5. **Post-route fixes** (`hand-route-net.py --maze`, a multi-layer A* checked against the routed board):
- the LED1 line, which Freerouting left open;
- a VSYS feeder from the PMIC to the nRF9151 ferrite on the 1 oz outer layers.
Supply-path resistance (`scripts/supply-resistance.py`):
| Path | Resistance | At 0.5 A |
| --- | --- | --- |
| VSYS: nPM1300 → FB1 | 59 mΩ | 30 mV |
| VBAT: JST → nPM1300 | 19 mΩ | 10 mV |
| VBUS: USB-C → nPM1300 | 44 mΩ | VBUS stays 0.15 mm next to the CC lines; only the charge current flows |
| VDD_NRF: FB1 → SiP | 4 mΩ | 2 mV |
The physical checks, which exist because core reports nothing when routing is disabled:
- `check-connectivity.py`: union-find over the copper tscircuit actually draws. Trace ends must be on the land's layer, vias carry their own net, and pour islands are included.
- `check-clearance.py`:
- trace, via and land spacing;
- copper lying inside another net's pour;
- drill-to-drill spacing of 0.2 mm or more;
- vias in pads.
- `check-holes.py`: no peg or shell hole through another part's land.
Two tscircuit behaviours are handled in `lib/geometry.ts` (`throughVia`). Both would otherwise flood the GND pours over foreign copper (shorts):
- A pcbPath via only gets the layers between its from and to layer, and the pour solver cuts antipads on those layers alone.
- The pour solver ignores trace segments that end on a via.
## Sleep budget (typical, datasheet figures)
| Consumer | State | Current |
| --- | --- | --- |
| nRF9151 | PSM floor | 2.7 µA |
| nPM1300 | BUCK1 auto mode, charger idle | ~0.8 µA |
| LIS2DW12 | low-power 12.5 Hz wake-up | ~1 µA |
| RT9080 + nRF7002 | LDO disabled (nRF7002 unpowered) | < 0.1 µA |
| SKY65943 | VEN low | 0.1 µA |
| NX3DV2567, TPD3F303, pull-ups | idle | < 1 µA |
| **Total** | | **≈ 5 µA** plus LTE/GNSS/Wi-Fi activity |
Activity dominates. One A-GNSS fix, one Wi-Fi scan (~60 mA for ~1–2 s) and one LTE-M report every 10 minutes average roughly 0.25–0.3 mA. That's about 2 months from a 500 mAh cell, and far longer with motion-gated reporting.
## Known limitations / bench gates
- **RF has not been measured.** The LTE and Wi-Fi feeds rely on external 50 Ω antennas. Tune the reserved LTE pi-match and check GNSS C/N0 in the final enclosure.
- **nRF7002 layout** follows Nordic's reference circuit but not their exact reference layout. Nordic isolates the buck-ground capacitors on a separate island tied to the paddle; here they share the ground net. Check Wi-Fi scan sensitivity on the bench.
- **JST polarity:** there is no reverse-battery protection. A reversed cell will destroy the nPM1300.
- **eSIM:** the MFF2 land is do-not-place. Order the eUICC from the connectivity provider and have it consigned to JLCPCB or hand-soldered.
- **Via drill:** 0.2 mm vias carry a small JLC surcharge (below their 0.3 mm free tier).
- **Via-in-pad:** five lands carry a via in the pad because no other spot exists:
- the edge-side ground pads of C20, C22 and R6;
- the crystal's XOP pad (X1.X1);
- the PAVDD 0201 capacitor C52.
Order the board with **epoxy-filled, capped vias** (JLC "via-in-pad"). Otherwise expect solder wicking on those pads, which matters most for the 0201 and the crystal.
- **VBUS** is routed at 0.15 mm (44 mΩ). Keep the nPM1300 charge current at or below about 500 mA, which is plenty for a small LiPo.
- **Routing is generated.** After any placement change, re-run `bun run route:all`. Freerouting may leave a different connection open each time; close it with `hand-route-net.py --maze` and check with `bun run check:routed`.