auto-sync 2026-09-01 18:00

This commit is contained in:
Leopoldo Rodríguez 2026-09-01 18:00:17 -06:00
parent 0a50ff7d21
commit 1044e706e5
8 changed files with 2132 additions and 1 deletions

View File

@ -0,0 +1,63 @@
# EinarBoard v5.5 — Auditoría EMC y fundamentos de layout (2026-08-22)
Contra el `.kicad_pcb` enviado a fabricar (15 pzas). Objetivo: decidir qué se corrige **antes de
la segunda corrida** y qué pasa al diseño v6 (ZED-F9P + 4G on-board). Stackup: F.Cu señal+pour
GND · In1 plano GND · In2 plano 3V3 · B.Cu señal+pour GND — topología correcta de 4 capas. ✔
## 🔴 Crítico — corregir ANTES de la segunda corrida
**1. `/SW_BAT` (nodo de conmutación del LTC4040) es una sola pista de 0.3 mm × ~15 mm.**
U6 pines SW → L6 (inductor 2.2 µH/8 A) va todo en 0.3 mm por F.Cu. En modo respaldo (boost desde
batería) ese nodo lleva la corriente completa del inductor: con 5 V/2.5 A de carga desde 3.7 V son
**~4 A promedio** — una pista de 0.3 mm/1 oz se calienta decenas de °C y tira tensión. El
datasheet del LTC4040 pide SW ancho y corto. *En la corrida actual:* funcionará en respaldo ligero;
vigilar en banco el caso "apagón + ráfaga TX del 4G". *Corrección:* polígono o pista ≥2 mm.
**2. `/BAT_PLUS` — los últimos ~20 mm hacia U6/C35 quedan en 0.3–0.5 mm.**
El tramo largo P4→(columna x≈193) va bien (1.0 mm en B.Cu + paralelo In2), pero la aproximación
final a los pads BAT es una cadena de 0.3 mm. Misma corriente que SW_BAT (2.4 A carga / ~4 A
descarga). *Corrección:* ensanchar a ≥1.5 mm + vías múltiples en el último tramo.
## 🟠 Importante — corregir en la segunda corrida si se puede, obligatorio en v6
**3. Lazo de retroalimentación del buck del ventilador (U2):** el divisor R20/R21 está a ~25 mm
del pin FB y la pista `/FB_FAN` recorre **60.7 mm**, con un punto a ~0 mm del cobre de `/SW_FAN`
(no es corto — DRC limpio — pero es acoplamiento directo del nodo ruidoso al nodo de alta
impedancia). Riesgo: jitter/rizado en 5V_FAN y EMI radiada. *Corrección:* R20/R21 pegados al pin 4,
FB corto y lejos de SW. (U7 y U12 están decentes: FB 8–9 mm, separación ≥2 mm.)
**4. Lazos calientes de entrada de los bucks:** Cin más cercano a 5–8.5 mm del pin VIN
(U2→C11 6.3 mm · U7→C40 5.1 mm · U12→C101 8.5 mm). Regla: <3 mm. *Corrección:* un 1 µF+10 µF
pegados a VIN-GND de cada MP1584.
**5. ESD del puerto DFU lejos del conector:** D14/D15/D16 (PESD) están a ~41 mm de J11, junto al
MCU. Clampean, pero el tramo conector→clamp radia durante la descarga. *v6:* moverlos a <10 mm
de J11. **J9 (USB del módem) no tiene ESD** — es arnés interno, aceptable, pero añadirlo en v6.
## 🟡 Menor — anotar para v6
- **Cristales lejos de sus chips:** Y3 HSE a 11.9 mm de U16, Y4 LSE a 15.2 mm, Y1 a 12.3 mm de
U11. El plano GND de In1 blinda lo que cruza por capas inferiores (verificado: los cruces de
/KCOIL, /I2C, /MAIN5V van bajo el plano), así que funciona — pero en v6 pegarlos a <5 mm y en
paquetes 3225 en vez de HC-49.
- **Par ETH RX desbalanceado:** RXP 16.1 mm vs RXN 23.6 mm (Δ7.5 mm). 100BASE-TX lo tolera de
sobra funcionalmente; en v6 rutear como par acoplado y igualar.
- Celdas de 20 mm sin vía de costura: 8 (mayoría bajo el zócalo del Ardusimple). Densidad global
buena: 165 vías GND, mediana vecino-más-cercano 4.6 mm. ✔
- Stub único: cola de 5.6 mm de /3V3 en In2 — es el propio plano de 3V3, inofensivo.
## ✔ Verificado y sano
Desacoplos: 12 caps ≤4 mm de su pin; los "lejanos" (C78/C30/C10) son bulk de entrada junto a los
conectores — colocación correcta. Pares USB FS: J9 Δ1.5 mm, J11 ok — Full-Speed lo permite.
Protección 24 V: D10 (SMBJ33A) a 9.9 mm de P1 y Q10 a 11.7 mm — bien. Anchos de potencia
restantes: /SW_5V 1.0 mm continuo, /24V_IN 1.0 mm, /FAN_RET 1.0 mm ✔ (los mínimos de 0.25 en
VEH5V/MAIN5V/3V3 son taps de sensado, no portadores). Zona keepout bajo el magjack presente ✔.
Costura GND y planos: correctos.
## Para el arranque de v6 (ZED-F9P + 4G on-board)
Heredar de esta lista: SW/BAT anchos desde el día uno, Cin en el pin, FB corto, cristales 3225
pegados, ESD en cada conector externo, pares diferenciales acoplados (el F9P añade USB y el 4G
añade USB/UART de alta velocidad + **RF**: la antena LTE exige keepout de cobre, 50 Ω controlada
y tierra sólida — eso define el stackup antes de colocar nada).

View File

@ -0,0 +1,657 @@
---
title: EinarBoard v6 — On-Board GNSS + 4G Integration — Design Brief & Execution Prompt
tags: [project-drogheda, hardware, einarboard, v6, gnss, rtk, 4g, lte, rf, design-brief, prompt]
project: Project Drogheda
author: Helgen CTO + Claude (decisions opened 2026-08-24)
date: 2026-08-24
status: Draft v1.0 — planning. §0 decisions locked by Leo 24-Ago; §1 GNSS choice is the one open gate.
authority: supplements [[watchdog-electrical-design-state]] (canonical) and inherits the fix list of
`2026-08-22_Drogheda_EinarBoard-v5_EMC-Audit_v1.0.md` in full. Where this conflicts with the 3.0
brief on v6 scope, THIS file wins.
---
# EinarBoard v6 — Design Brief ("the prompt")
> **What this is.** The instruction set for turning `hbox-watchdog-v5` (EinarBoard 3.0 / v5.5,
> 15 pcs in fab since 22-Ago) into **EinarBoard v6** — the first HBOX board with the **GNSS
> receiver and the 4G modem soldered on**, retiring the simpleRTK2B Pro shield (MOD1) and the
> XBee-socket 4G NTRIP Master (J7/J8).
>
> Standing rules unchanged: **file-level KiCad edits only**; git checkpoint after every phase;
> every structural edit verified by an independent parse; bench gates before fab.
---
## 0. Decisions locked 2026-08-24 (Leo)
| Topic | Decision |
|---|---|
| Scope | **Full integration** — GNSS + 4G both on-board; MOD1 and J7/J8 deleted |
| Board outline | **Keep 140.000 × 100.000 mm** (x ∈ [60.19, 200.19], y ∈ [40.32, 140.32]) — no enclosure rework |
| SIM | **MFF2 soldered SIM** (DFN 5×6). No tray, no spring contacts, no opening a sealed IP67 box in Veracruz |
| **GNSS receiver** | 🔒 **LOCKED — ZED-F9P-04B, LCSC `C5120257`** (Leo, 24-Ago). Rationale §1. LG290P stays as the documented v7 cost-down |
| **4G module** | 🔒 **LOCKED — SIMCom A7670SA-LASC, LCSC `C18548276`** (Leo, 24-Ago). LTE Cat-1 + **2G GSM** fallback; 3G answered in §2.3 |
| Inherited | The **entire v5.5 EMC fix list** is day-one scope, not a wish list (§7) |
**Still open — need Leo:** antenna interface (§4.4), modem VBAT source sign-off (§3.2),
homologación path (§9.4), and gate **T1** (§2.4) — which does **not** block the schematic, only
the decision to stay on the A7670SA if the cabinet runs hot.
---
## 1. GNSS receiver — "cheapest" vs "best value" are not the same part
### 1.1 The hard constraint that eliminates the truly cheap tier
Your base is the Ardusimple **AS-PRO-CORS = ZED-F9P = L1/L2**, and Mexican public CORS (INEGI
RGNA) are overwhelmingly **GPS L1/L2 + GLONASS**. RTK requires the rover to track the same
frequencies the base broadcasts.
That **disqualifies the entire cheap L1/L5 tier**: Quectel LC29H (all variants, $22–33),
ZED-F9P-**15B**, NEO-F9P, LG69T. They are L1 + L5, **no L2**. Fed from an L1/L2 base they
degrade to effectively single-frequency — you would be paying for RTK silicon and getting
float. Do not let the $22 LC29H price tempt anyone: it is the right part for a product where
you also own the base, and the wrong part for a fleet you diagnose remotely from Querétaro.
Also permanently out: ATGM332D / ATGM336H / AT6668 ($1.44–4.75, huge LCSC stock) — **single-band
L1, no carrier-phase RTK at all.**
### 1.2 The three that survive
| | **ZED-F9P-04B** | **LG290P03AAMD** | **UM980** |
|---|---|---|---|
| Vendor | u-blox (CH) | Quectel (CN) | Unicore (CN) |
| Bands | L1/L2 | **L1/L2/L5/E6** | all incl. E6/B2b |
| RTK rate / convergence | 7–24 Hz / <12 s | 20 Hz / **5 s** | 50 Hz / <5 s |
| Package | 54-LGA 22×17×2.4 | **12.2×16×2.6** | 54-LGA 22×17×2.6 |
| Supply / current | 2.7–3.6 V, 85 mA trk (130 pk) | 3.15–3.45 V, **91 mA** | 3.0–3.6 V, **145–180 mA / 480 mW** |
| Internal LNA | **No — active antenna mandatory** | ambiguous (see §1.5) | **No** |
| USB | **Yes** | **No** | No |
| qty 1 | **$84.82 LCSC** (C5120257) / $131.74 Mouser | $79.89 | ~$94.88 |
| qty 100 | $116.56 Mouser | **$59.54** | ~$80.65 |
| JLCPCB | C5120257, Extended, 104–129 stk | **not listed** — consign | C9900015354, Extended |
### 1.3 The money, honestly
| Volume | ZED-F9P (LCSC) | LG290P | Saving by switching |
|---|---|---|---|
| 30 units | $2,545 | ~$2,100 | **~$450** |
| 100 units | $8,482 (if stock holds) | $5,954 | ~$2,500 |
| 300 units | ~$34,968 (Mouser $116.56) | $17,862 | **~$17,100** |
Against that saving, switching away from u-blox costs you, **once**:
- **The entire config toolchain.** u-center, UBX-CFG-VALSET, your saved receiver configs, your
RAM-vs-BBR/Flash lesson from the June RCA — all of it is u-blox-specific. LG290P configures
with `$PQTM…` sentences.
- **The backend decoder.** htracker already ingests `$PUBX,00` (retranslator +138, delivered
10-Ago). LG290P does not emit PUBX. That's a protocol change on the server too.
- **A fresh RTCM-compatibility unknown**, in a program whose *actual* June root cause (free
rtk2go caster, RAM-only config, unverified UART2 path) is still not fully closed.
**Read: at 30 units the saving is $450 — less than a week of firmware and backend work. At
300 units it is $17k — real money.**
### 1.4 🔒 DECIDED 24-Ago — ZED-F9P-04B
> **v6 = ZED-F9P-04B, sourced from LCSC (C5120257) at $84.82.**
> It is $32/unit cheaper than Mouser with *zero* engineering change, it is the receiver your
> base station is paired to, and it keeps u-center as your field-diagnosis tool while you are
> simultaneously taking on an RF front end, a cellular radio, and an NTRIP client you've never
> shipped. **One new unknown at a time.**
>
> **Documented cost-down for the 300-unit phase = LG290P.** Scope it as a bounded respin of the
> GNSS island only (§5 puts the receiver in its own corner precisely so this stays cheap), plus
> the firmware/backend work, and pull the trigger when volume pays for it.
⚠ **Sourcing caveat that applies either way:** LCSC shows ~104–129 pcs of ZED-F9P-04B. A
100-unit run eats the entire bin. Mouser has 573 + 834 arriving, 18-week factory line. **For a
30–100 unit run, buy the receivers yourself and consign them to JLCPCB** — you control the most
expensive, longest-lead line item instead of being hostage to an LCSC bin.
*(If you want the LG290P now anyway, say so and I'll re-baseline §5/§6 — but then Phase 1 in
§10 grows a receiver-bring-up spike and the backend decoder becomes v6 scope.)*
### 1.5 Open verification if LG290P is ever chosen
Its datasheet says "external active antenna"; Mouser's part description says "**w/ LNA
(KT5030)**". These cannot both drive the RF section. **Resolve against the LG290P Hardware
Design doc before laying out one millimetre of RF.**
---
## 2. 4G modem
### 2.1 Bands decide the variant before anything else
Telcel = **B4** (bulk) + B7 + B66. AT&T MX = B2/B4/B5/B7. Altán "Red Compartida" = **B28**
(700 MHz — the best-propagating band in the country). Movistar rides AT&T's RAN.
→ You need **B2/B4/B5/B7/B28/B66**. That is exactly the **`-SA`** (SIMCom), **`-LA`** (Quectel
EG915x) and **`-AU`** (Quectel EC200x) suffixes. **Any `-EU` / `-E` / `-CN` / `-C` part is
useless here** — no B4, no B66. This is the single most common way this decision goes wrong.
### 2.2 🔒 DECIDED 24-Ago — SIMCom A7670SA-LASC
> **SIMCom A7670SA-LASC — LCSC C18548276, $7.58 @1, $7.39 @10, ~$6.3–6.9 @1k, 220 in stock,
> already in the JLCPCB parts library** (Extended, X-ray inspection required, MSL3).
Why it wins on your criteria:
- **Cheapest thing that is actually assemblable at JLCPCB.** The only real alternative in the
JLC library is Quectel EG915U-LA (C19185632) at ~$9–13.
- **Correct LatAm bands**: LTE-FDD B1/2/3/4/5/7/8/**28**/**66**, plus GSM 900/1800.
- **Single antenna.** Cat-1bis-class: one `RF_ANT` pin, no RX-diversity port. One less 50 Ω
path, one less antenna, one less thing to get wrong. (This is also why **Cat-4 is the wrong
choice** — EC200A/SIM7600 add a second antenna port, a 29×32 mm footprint and 2–4× the price
for throughput you will never use on a 50–200 MB/month NTRIP + NMEA stream.)
- **Public docs, no NDA**, and the A76xx AT set is SIM7600-compatible → **TinyGSM works**, which
is what makes the §2.5 firmware estimate credible.
- **Feature-variant upgrade path with no respin**: `-LASC` (bare) → `-LASE` (+BLE) → `-FASE`
(+GNSS +BLE) are the same 88-LGA footprint.
**Not interchangeable, do not confuse:** A7670 = 88-LGA · A7672 = 124 hybrid LCC+LGA ·
SIM7070 = 68-LCC. All 24×24 mm, all different footprints. Migration = respin.
### 2.3 On your 3G question — the honest answer
**3G is not the lever you want, and it is on its way out.** Mexico's 2G/3G shutdown is still
under negotiation rather than executed (operators were still discussing it in July 2026 —
millions of handsets block it), so 3G *exists* today. But designing a 2026 board with a 5–10
year fleet life around a network the regulator and all three carriers are actively trying to
switch off is buying a fallback with an expiry date.
What actually fixes poor coverage, in order of effect:
1. **Band coverage at low frequency.** B28 (700 MHz) and B5 (850 MHz) penetrate buildings and
container stacks far better than B4/B66 (1700/2100 MHz). The A7670SA has all of them.
2. **Cat-1bis link budget.** A narrower, lower-order category has a better link budget than
Cat-4 at the cell edge. The cheap choice is also the more robust one here.
3. **A proper external antenna on a bulkhead SMA**, not a chip antenna inside a metal-adjacent
cabinet. §4.4.
4. **2G/GSM fallback** — which the A7670SA already has (GSM 900/1800). At 20–40 kbps GPRS you
will not hold RTCM at full rate, but you keep telemetry and remote command alive, which is
what "never vanish" (invariant #2) actually requires.
**If you still want WCDMA/3G**, the cheap route is **SIM7600SA-H** (Cat-4 + WCDMA + GSM, LatAm
bands) at ~$25–40 — but you pay ~4× the module cost, a second antenna port, a 29×32 mm
footprint, and more heat, for a network that is being switched off. **My recommendation is no.**
Take the A7670SA's 2G fallback and spend the difference on the antenna.
### 2.4 Temperature — the one place I'd push back on "cheapest"
Your own RCA names the 4G modem as a heat casualty in the nested cabinet under Veracruz sun.
| Part | Operating range |
|---|---|
| SIMCom A7670SA | **−30…+80 °C** (−40…+85 degraded) |
| Quectel EG915U-LA | **−35…+75 °C** (−40…+85 extended) |
| Telit LE910C1-LA (~$48) | **−40…+85 °C full** |
| u-blox LARA-R6001 (~$43–55) | **−40…+85 °C full** |
> 🔴 **GATE T1 — cabinet temperature. YOU ALREADY HAVE THIS DATA. DO NOT INSTRUMENT ANYTHING.**
>
> Fielded V3 units already report **P12 = 10 k NTC → PB1 → `tntc` ("Cámara" = cabinet
> interior)** and **P5 = SHT40 → `tair`**, inside `$PHLG` every 30 s, and the htracker
> retranslator has been ingesting `$PHLG` in production since 10-Ago. **Query the existing
> telemetry history for the max `tntc` per unit over the hottest days.**
>
> That query — free, today, no hardware, no truck roll — settles the single largest cost
> decision in v6: an **$8** A7670SA versus a **$48** Telit LE910C1-LA. Across 300 units that
> is a **$12,000** decision sitting in a database you already own.
>
> Decision rule: **max `tntc` < 70 °C → A7670SA.** 70–75 °C → A7670SA **plus** the M2.5
> heatsink clamp (§5) and re-measure on the first article. **> 75 °C → escalate the tier**;
> you already know what a modem failure costs at ICAVE.
### 2.5 What the modem costs you in firmware
**Neither the A7670SA nor any Quectel module has a built-in NTRIP client.** That was the
Ardusimple 4G NTRIP Master's entire value, and you are giving it up. You implement NTRIP v1
yourself: `GET /MOUNTPOINT HTTP/1.0` + `Authorization: Basic …`, then read raw RTCM3 off a
transparent TCP socket and shovel it into the receiver.
Realistic budget: **1–2 weeks**, not 1 day — the AT/socket layer is a day; reconnect/backoff
logic, watchdog integration, TLS-on/TLS-off caster handling, signal-quality telemetry into
`$PHLG`, and field failure modes are the rest.
> ✅ **This risk can be retired for free, right now.** See §10 Phase 1: the **XBee socket on the
> v5.5 boards currently in fab** will host an A7670SA breakout. You can prove the entire
> NTRIP-in-STM32 stack on hardware you already paid for, while the boards are in transit,
> before a single v6 trace exists.
---
## 3. Power architecture — the biggest new electrical work
### 3.1 What the modem demands
| Parameter | A7670SA | Consequence |
|---|---|---|
| VBAT | 3.4–4.2 V (typ 3.8) | **Not the 3V3 rail. Ever.** |
| Peak current | **2.8 A** (GSM TX burst) | |
| Droop limit | **≤300 mV** at the burst | |
| Bulk | **≥300 µF** if the source truly delivers 2.8 A; **≥1000 µF** if not | |
| VBAT trace | **≥3 mm** wide or equivalent pour | |
| VBAT pins | **All three (55/56/57) must be connected** | RF performance depends on it |
| I/O rail | **1.8 V** | **Level shifting to the 3.3 V STM32 is mandatory** |
| VDD_1V8 | output, 50 mA max | level-shift reference only, not a system rail |
### 3.2 🔴 The 10-Ago fleet boot-loop makes this decision for us
**Read the 10/11-Ago incident before designing this rail.** Root cause on record: a flat 12 Ah
pack demanding 2.42 A **plus the LTE attach burst** exceeded the **SD-15B-05's 3 A**, while the
**LTC4040 input limit (3.12 A) is set ABOVE the supply's rating** → rail sagged to the PFI
4.60 V trip → failover to a flat pack → brownout → reboot → loop. The modem stayed "invisible"
because it never finished its 30–60 s LTE attach inside the loop. Standing HW errata: *"input-limit
resistor must come DOWN below PSU rating (or bigger PSU)."*
**v6 adds ~1.6 A of on-board modem to that same 5 V rail.** Naively bucking 3.8 V off MAIN5V
would aim the new radio straight at the failure mode that already took the fleet down.
| | (a) Direct from the 1S pack | (b) Buck from MAIN5V | (c) **Buck from OR(24V_IN, MAIN5V)** ← recommended |
|---|---|---|---|
| Load on the SD-15B 3 A budget, normal op | none | 🔴 **+1.6 A — the 10-Ago failure mode** | **none — runs off 24 V** |
| Alive during blackout | ✔ | ✔ | ✔ (falls back to MAIN5V ← pack ← LTC4040) |
| Bench without a pack | ✘ | ✔ | ✔ |
| Under-voltage risk | 🔴 pack sags below the 3.4 V modem floor | ✔ | ✔ |
| Couples TX bursts onto | 🔴 the LTC4040 BAT node — flagged 🔴 in the EMC audit | the 5 V rail | 24 V (normal) / 5 V (blackout only) |
**Recommendation (c) — one buck, two inputs.** A **4.5–40 V input, 3 A synchronous buck** (e.g.
MP4560 / AP64350 / TPS54331 class — *not* a fourth MP1584, which is already the source of EMC
findings 3 and 4) whose input is an **ideal-diode / PMOS OR of `24V_IN` and `MAIN5V`**, output
3.8 V, feeding the modem through a **P-FET load switch under MCU control (`MODEM_PWR_EN`)**.
Why this is the robust *and* cheap answer, for roughly **$1.50 more** than option (b):
- With the cabinet live, the modem's ~9.5 W comes off **24 V at ~0.45 A** — the SD-15B never
sees it, and the 10-Ago brownout path is structurally removed rather than merely re-tuned.
- In blackout the same buck accepts MAIN5V and the radio keeps reporting — invariant #2 holds.
- One converter, one inductor, one BOM line. D10 (SMBJ33A) already clamps the 24 V input.
- `MODEM_PWR_EN` gives the watchdog authority to hard power-cycle a wedged radio — invariant #3.
> ⚠ **Still fix the input-limit errata.** Even with the modem off the 5 V rail, the LTC4040's
> input limit must come below the PSU rating (or the PSU must grow). v6 is the board where that
> gets corrected, not deferred again.
> ⚠ **And this makes the v5.5 🔴 EMC findings load-bearing.** In blackout MAIN5V does feed the
> modem, so the LTC4040 boost hits the ~4 A `/SW_BAT` case the EMC audit flagged. **`/SW_BAT`
> ≥2 mm polygon and `/BAT_PLUS` ≥1.5 mm + via farm are v6 hard requirements, not second-run
> nice-to-haves.**
### 3.3 GNSS rail — low-noise, non-negotiable
The F9P and UM980 have **no internal SAW**. This board carries **three MP1584 switchers**.
They will happily lock onto your buck's harmonics.
→ **Dedicated low-noise LDO** off 3V3 for the GNSS island (not a buck, not a shared tap),
placed on the GNSS island itself. Verify the **ZED-F9P VCC ramp rate ≤ 20 µs/V** — that is a
*damage* limit, not a guideline; check it against the LDO's soft-start.
### 3.4 Budget delta (worst case, backup mode)
| Load | Was (v5.5) | v6 |
|---|---|---|
| 4G | via 3V3_MODEM to XBee socket, ~1.5 A pk @3.3 V | **2.0 A pk @3.8 V ≈ 7.6 W → ~1.6 A from MAIN5V** |
| GNSS | via MOD1 shield off 5 V | 85–130 mA @3.3 V (F9P) |
| Everything else | unchanged | unchanged |
Recompute the LTC4040 backup runtime against the 1S4P ~12 Ah pack once (b) is locked.
---
## 4. RF — the part that decides the stackup before anything is placed
### 4.1 Stackup
Current topology is already correct and stays: **F.Cu signal+GND pour · In1 solid GND ·
In2 3V3 plane · B.Cu signal+GND pour.**
New requirement: **order the board as controlled impedance** (JLCPCB 4-layer, JLC04161H-7628;
F.Cu→In1 prepreg ≈ 0.2104 mm). Target **50 Ω CPWG on F.Cu referenced to In1** — roughly
0.35 mm trace / 0.25 mm gap at εr ≈ 4.4, **to be confirmed against JLCPCB's own impedance
calculator before layout**, not after.
Absolute rules for both RF traces:
- Referenced to the **In1 GND plane with no split, ever** — no plane cut, no via field gap, no
In2 3V3 island underneath.
- As short and straight as possible; ground-stitched along **both** sides at ≤ λ/20.
- No other signal crosses them on any layer.
### 4.2 GNSS RF chain
Active antenna (mandatory for F9P/UM980) → bias tee: **≥68 nH 0603 inductor** (self-resonance
above 1.6 GHz) + DC block on the RF path + 100 nF ∥ 100 pF on the bias node + **ESD diode rated
>2 GHz** at the connector.
Wire **`ANT_DETECT` and `ANT_SHORT_N`** to the MCU (or a spare PCF8574 pin). Free antenna-fault
telemetry — and you have already lost two units (H0005, H0014) to 0-sats antenna faults you
could only diagnose by reading logs.
⚠ **Do not use `VCC_RF` as the antenna bias on UM980** — Unicore explicitly says its protection
is inadequate. Feed the antenna from a separate current-limited rail.
### 4.3 🔴 The #1 technical risk in this whole change: GNSS desense
A 23 dBm LTE transmitter drawing 2–3 A bursts, 3 cm from a GNSS front end, is a classic
RTK-killer — LNA compression and broadband noise will cost you exactly the fix you are paying
for. **The Ardusimple shield has been hiding this problem from you. A bare module hands it to
you.**
Mitigations, in order of effect:
1. **Maximum board separation** — §5 puts GNSS in the bottom-left corner and LTE at the right
edge: **~149 mm apart, essentially the full board diagonal.** This is the whole reason the
floorplan is shaped the way it is.
2. **Antenna separation ≥ 20 cm** on the enclosure. **Treat this as a design rule, not an
optimization.** Two separate bulkheads, as far apart as the cabinet allows.
3. Route the modem's return path away from the GNSS island; no shared reference between the
two RF regions except the solid In1 plane.
4. **Front-end filtering.** Straightforward for an L1-only receiver; **for a multi-band receiver
a single 1575 SAW would kill L2/L5**, so rely on an active antenna with good out-of-band
rejection + separation. Specify the antenna accordingly.
5. **Measure it.** Gate G3 in §10: fix quality with the modem idle vs. transmitting at full
power. This is a pass/fail bench test, not an opinion.
### 4.4 Antenna interface — OPEN, needs Leo
Recommendation: **2× U.FL on board → pigtails → 2× SMA bulkhead on the enclosure**, placed as
far apart as the cabinet allows. This is what makes mitigation #2 possible; an on-board or
short-pigtail arrangement forfeits it.
Needs from you: does the current enclosure have room for a second bulkhead, and where? That
answer changes where the U.FL connectors sit, which changes the floorplan.
### 4.5 LTE link budget — the module is not the variable
**Every LTE module here transmits at the same power.** 3GPP Power Class 3 is **+23 dBm ±2 dB**,
and it is a conformance requirement, not a feature — the $8 A7670SA and the $48 Telit put out
the same watt. Receive sensitivity is likewise floored by 3GPP; premium parts buy perhaps
**1–3 dB** of margin, not a different class of performance.
What actually sets the link budget on this product, in descending order of effect:
| Factor | Typical swing | Notes |
|---|---|---|
| **Antenna type and placement** | **10–20 dB** | An antenna inside or flush against a metal cabinet on a metal crane is the whole ballgame |
| **Band in use** | 6–10 dB | **B28 (700 MHz)** and B5 (850 MHz) penetrate structure far better than B4/B66 at 1700/2100 MHz. The A7670SA has all of them |
| **Cable / pigtail loss** | 1–3 dB | Keep the LTE pigtail short; every extra 300 mm of thin coax at 1.9 GHz is real |
| **Cat-1bis vs Cat-4** | 1–2 dB in our favour | Lower-order category = better cell-edge behaviour. The cheap choice is also the more robust one |
| **Module vendor** | **1–3 dB** | The only line the extra $40 buys |
> **You already have the field measurement.** The `$GPTXT,NET,H00xx,TELCEL,-87,334 020` line
> the fleet emits carries the modem's **RSSI in dBm** and the operator's MCC/MNC (334/020 =
> Telcel MX). The example on record shows **−87 dBm**, which is a comfortable working level.
> **Query the fleet for the per-unit distribution over the last month, the same way as gate T1.**
> If the worst unit sits near −105 dBm you have an antenna/siting problem to solve — and no
> module in this survey would have solved it. Phase 1 also adds `csq` to `$PHLG` (field 30),
> which makes this permanently visible instead of a one-off query.
**Conclusion: spend on the antenna and its mounting, not on the modem.** A better bulkhead
antenna, a shorter pigtail and a clear mount are worth 10 dB; a more expensive module is worth 2.
### 4.6 🔴 GNSS multipath — the environment problem, and what actually fixes it
A container terminal is close to a worst case: stacked steel boxes, RTG and STS cranes,
reach stackers, all of it moving. **Multipath is a receiver-and-antenna problem. No modem
choice affects it, and no cabling change fixes it.**
**First, make sure it is actually the diagnosis.** As of 10-Ago the fleet showed **zero
corrections applied anywhere** — that produces SPS, full stop, and it looks nothing like
multipath. **Multipath presents as float-instead-of-fixed, or a fix that wanders**, and it can
only be diagnosed *after* RTCM is confirmed arriving. Phase 1's new telemetry separates the
two for the first time:
| `rtcm_msgs` | fix quality | Diagnosis |
|---|---|---|
| 0 / not rising | 1 (SPS) | **link or caster** — not multipath |
| rising | 1 (SPS) | receiver config, or base/rover **band mismatch** |
| rising | 5 (float), never 4 | **multipath / sky obstruction** — this is the real thing |
| rising | 4 but `hAcc` noisy | multipath, partially mitigated |
**Mitigations, cheapest and most effective first:**
1. **A ground plane under the patch antenna.** A metal disc **≥ 100 mm diameter** beneath a
patch kills ground-bounce and is the single best return per peso in this list. Often free.
2. **Mount height and clear sky.** Highest point of the machine, away from vertical steel.
On an RTG that is the top of the gantry, not the cabinet lid.
3. **A better antenna.** A multiband antenna with good **axial ratio** and suppressed gain at
low elevation rejects reflections that arrive from below the horizon. Budget US$60–120 —
still cheaper than a receiver upgrade and more effective for this failure mode.
4. **Raise the elevation mask** in the receiver (10–15°, `CFG-NAVSPG-INFIL_MINELEV`) to discard
low-elevation reflected signals, and set the correct **dynamic model** for the machine.
5. **Keep L2.** Dual-frequency is materially better at ambiguity resolution under multipath —
another reason the L1/L5-only cheap tier was the wrong answer (§1.1).
6. **Log C/N₀ per satellite** at a problem site. A healthy sky and a reflective sky look
completely different, and it turns an argument into a measurement.
**If — after 1–6 — multipath is still the binding constraint**, then and only then does silicon
help, and the honest options are: **ZED-X20P (+$31/unit**, same 54-LGA 22×17 footprint, so
Block N accepts it with a pin-map check, plus better interference mitigation and 55 mA instead
of 85 mA) or **Septentrio mosaic-X5** (genuinely best-in-class multipath rejection, but 3–5×
the F9P and no LCSC/JLC presence). **Do not buy either on suspicion — buy it on a C/N₀ log.**
---
## 5. Floorplan on 140 × 100 — proposed
Verified from `hbox-watchdog-v5.kicad_pcb` (228 footprints, board x ∈ [60.19, 200.19],
y ∈ [40.32, 140.32]).
**Freed by deletion:**
| Deleted | Ref | Position | Area recovered |
|---|---|---|---|
| simpleRTK2B Pro UNO socket | **MOD1** (32 pads) | (90.6, 129.9) | **bottom-left, ~68 × 53 mm** |
| XBee sockets, 2.0 mm | **J7, J8** (10 pads each) | (174.2, 74.8) / (196.5, 74.8) | **right edge, ~27 × 20 mm** |
| Shield / heatsink bolts | MH7, MH9, MH10, MH11 | various | reclaim as GND stitching (the EMC audit found 8 empty 20 mm stitching cells, **mostly under MOD1**) |
**Proposed placement:**
```
x=60 ┌────────────────────────────────────────────────────┐ x=200
y=40 │ [J10 RJ45] connector row: P5 P10 P2 P11 P6 P8 P12 P4 │
│ │
│ [U11 W5500] [U16 STM32F411] [U2/L2 fan] │
y=70 │ [SW1 SW2 J11] Y3 Y4 [U10 SC16IS] ┌──────────┤
│ [U14][U15] │ ★ LTE │ ← freed XBee area
│ [U13 PCF8574] │ U19 A7670SA
y=100│ │ MFF2 SIM │
│ ┌──────────────────┐ [U7] [U6 LTC4040+L6] │ 3.8V buck
│ │ ★ GNSS ISLAND │ [U12] [P14]│
y=140│ │ U18 + LDO + bias │ [P15][P16] [J9] [P1] │
└──┴──[U.FL GNSS]─────┴───────────────────[U.FL LTE]┘
```
- **GNSS island → the freed MOD1 area, bottom-left corner.** It is the furthest point on this
board from all four switchers (nearest: U7 at 140.0, 112.9 — ~60 mm; L6/U6 LTC4040 at
~173, 110 — ~95 mm), and it puts the U.FL at a board edge. The island holds only the
receiver, its LDO, its decoupling and its bias tee — **nothing else**, so a future LG290P
swap (§1.4) is a contained respin.
- **LTE island → the freed XBee area at the right edge**, next to the existing power cluster
where the current already lives. The 27 × 20 mm freed is **too tight for a 24 × 24 module +
≥300 µF bulk + buck + MFF2** — reclaim adjacent area by relocating **P14 (RGB_LED_EXT, at
195.1, 101.4)** and shifting the **U2/L2 fan buck** north. Budget ~35 × 30 mm.
- **Resulting GNSS ↔ LTE separation ≈ 149 mm** — the full diagonal. That is the point.
- Keep **2× M2.5 holes** near the modem for a heatsink clamp (§2.4).
---
## 6. Signal & connectivity delta
### 6.1 The good news: the MCU side barely changes
These nets already exist in `hbox-watchdog-v5.kicad_sch` and simply terminate on solder-down
silicon instead of a socket:
`GNSS_RX` · `GNSS_TX` · `DIN_4G` · `DOUT_4G` · `MODEM_RST` · `MODEM_LED` · `3V3_MODEM`
**No MCU pin reallocation is required for the basic paths.** USART1/2/6 are all on the F411RET6;
RS-485/RS-232 stay on the SC16IS752 over SPI.
### 6.2 New signals needed
| Signal | Where it goes | Note |
|---|---|---|
| `MODEM_PWRKEY` | spare **PCF8574 (U13)** pin | ⚠ **timing is vendor-specific**: SIMCom = low ≥50 ms to power on, ≥2.5 s to power off. Quectel = low ≥2 s on, ≥3 s off. **Never copy timing between vendors.** |
| `MODEM_STATUS` | spare PCF8574 pin | asserts ≈11.2 s after power-on (A7670); UART ready ≈8 s |
| `MODEM_PWR_EN` | GPIO → P-FET load switch | the hard power-cycle path (§3.2) |
| `GNSS_RESET_N` | GPIO | F9P needs a 7–13 kΩ pull-up |
| `GNSS_ANT_DETECT`, `GNSS_ANT_SHORT_N` | PCF8574 | free antenna-fault telemetry (§4.2) |
| `GNSS_TIMEPULSE` | GPIO / timer input | optional but nearly free |
| **RTCM path** | **modem UART → GNSS UART2, direct in hardware** | this is what the Ardusimple shield did internally; replicate it. MCU does the NTRIP handshake, then the RTCM3 stream flows without touching the MCU — exactly the Modo B architecture already engineered |
> **Action:** confirm free PCF8574 pins against the v5 netlist before Phase 2. `F0_SEL`,
> `F1_SEL`, `F2_SEL`, `EXT_LED`, `MODEM_LED`, `CHRG_STAT`, `CHRG_FAULT` are already on it.
### 6.3 Level shifting — mandatory, do not skip
Modem UART is **1.8 V**; the STM32 is 3.3 V. Use **TXS0108E** (what Quectel recommends) or the
discrete MMBT3904 network SIMCom documents. This is not a "3.3 V is close enough" situation.
### 6.4 MFF2 SIM
- DFN 5×6, on the LTE island, close to the modem.
- **ESD array at the SIM lines** (ESDA6V1W5 / SMF15C class).
- A7670SA has an internal 4.7 kΩ pull-up on USIM1_DATA; **USIM2_DATA would need an external
one** — you only need SIM1.
- 1.8 V and 3.0 V auto-detect, no strapping needed.
- ⚠ **Sourcing:** you need an actual MFF2 SIM from Telcel/AT&T MX or an M2M MVNO with a Mexican
profile. **Flat-rate IoT SIMs sized for sensors (500 MB / 10 years) are an order of magnitude
too small** for 50–200 MB/month. Start this conversation early — it has a lead time and it is
not an engineering task.
### 6.5 USB
**J9 (USB-C) is kept** and becomes the modem's USB — recovery path if a firmware update bricks
it, plus PPP/RNDIS debugging. **Reserve VBUS/D+/D−/GND and a `USB_BOOT` test point**; USB_BOOT
must not be pulled low at power-up. Add the ESD array that v5.5 does not have (**<2 pF** on the
data lines). 90 Ω differential.
---
## 7. Inherited EMC fix list — v6 hard scope
Straight from `2026-08-22_…EMC-Audit_v1.0.md`. **None of these is optional in v6:**
| # | Fix | v6 status |
|---|---|---|
| 🔴 1 | `/SW_BAT` → **≥2 mm polygon** (was 0.3 mm × 15 mm carrying ~4 A) | **Now load-bearing** — the modem adds ~1.6 A to MAIN5V in backup (§3.2) |
| 🔴 2 | `/BAT_PLUS` final approach → **≥1.5 mm + via farm** | same reason |
| 🟠 3 | Fan buck FB: **R20/R21 at pin 4**, `/FB_FAN` short and away from `/SW_FAN` | mandatory |
| 🟠 4 | **1 µF + 10 µF within 3 mm** of every buck VIN-GND (was 5–8.5 mm) | mandatory — and applies to the new 3.8 V buck |
| 🟠 5 | **ESD at every external connector, <10 mm** (D14/D15/D16 were 41 mm from J11) | mandatory + J9 now gets ESD it never had |
| 🟡 6 | Crystals **3225 package, <5 mm** from their ICs (Y1/Y3/Y4 are 12–15 mm away in HC-49) | do it |
| 🟡 7 | ETH RX pair **coupled and length-matched** (Δ7.5 mm today) | do it |
| 🟡 8 | Fill the 8 empty 20 mm stitching cells | free — they were mostly under MOD1, which is being deleted |
---
## 8. BOM & cost delta
**Removed per unit:** Ardusimple **4G NTRIP Master** (€112 @≥50 ≈ **$121**) + Ardusimple
**simpleRTK2B Pro** shield *(price to verify)* + MOD1 headers + J7/J8 XBee sockets.
**Added per unit (indicative):**
| Item | Cost |
|---|---|
| ZED-F9P-04B (LCSC C5120257) | $84.82 |
| A7670SA-LASC (LCSC C18548276) | $7.58 |
| MFF2 SIM | carrier-dependent |
| 3.8 V/3 A buck + inductor + ≥300 µF bulk | ~$2.50 |
| Low-noise LDO + GNSS decoupling | ~$0.80 |
| TXS0108E level shifter | ~$0.50 |
| 2× U.FL + 2 pigtails + 2 SMA bulkheads | ~$6 |
| RF passives, bias tee, ESD arrays | ~$2 |
| Controlled-impedance fab option | per-order, small |
| **Subtotal added** | **~$104 + SIM** |
**The headline: the 4G side alone saves ~$113/unit** ($121 Ardusimple → $7.58 module). Add the
simpleRTK2B Pro's margin over a bare F9P on top. At 30 units that is meaningful; at 300 it is
the difference between a viable and a non-viable unit price.
⚠ JLCPCB notes: both modules are **Extended** ($3 one-time setup each), both flagged **X-ray
inspection required**, both **Standard PCBA tier only** — no Economic assembly. A7670SA is
**MSL3** (168 h floor life ≤30 °C/60 %RH) — matters for JLC and for your own rework.
---
## 9. Risk register
| # | Risk | Severity | Mitigation |
|---|---|---|---|
| R1 | **GNSS desense from LTE TX** — you buy RTK silicon and get float | 🔴 | §4.3: 149 mm board separation, ≥20 cm antenna separation, measured at gate G3 |
| R2 | **Modem thermal** — cheap tier is −30…+80 °C, cabinet may exceed it | 🔴 | **Gate T1 (§2.4): soak a fielded unit now.** Escalate to Telit/u-blox if >75 °C |
| R3 | **NTRIP client in STM32** — new firmware on the critical path | 🟠 | **Phase 1: prove it on v5.5's XBee socket while the boards are in fab** |
| R4 | **Homologación** — the host now contains a radio (§9.4) | 🟠 | Start the lab conversation in Phase 0; it is calendar time, not engineering time |
| R5 | **Pack sag below the 3.4 V modem floor** | 🟠 | Resolved by §3.2 option (b); if (a) is ever chosen, add UV inhibit |
| R6 | **v5.5 EMC findings carried forward** | 🟠 | §7 — day-one scope |
| R7 | **F9P LCSC stock (~104–129 pcs)** | 🟡 | Consign from Mouser for anything ≥100 units |
| R8 | **First RF design in this program** | 🟡 | Controlled impedance ordered, In1 unbroken, external review of the RF section before fab |
### 9.4 Homologación — start this early
The regulator changed: **IFT was dissolved 17 Oct 2025 and replaced by the Comisión Reguladora
de Telecomunicaciones (CRT)** from 21 Oct 2025. Existing IFT certificates remain valid and
labels still carry the IFT number; procedures continue under the same technical norms.
The certificate is issued to **your product** (applicant + brand + model + year), not to the
module. Modular relief exists in practice — the host is labelled *"Este producto contiene el
módulo aprobado modelo XXXX, IFT No. …"* — but there is **no blanket exemption**. Applicable:
**IFT-008** (cellular) and **NOM-208 / NOM-221**. Testing in an accredited Mexican lab or a US
MRA-recognized lab, ~3–5 weeks.
Two consequences that bite:
1. **Freeze on one module before you file.** Switching modules later = new or amended certificate.
2. Sources **disagree** on whether the certificate holder must be the importer. **Get this pinned
down by the lab, in writing, in Phase 0.**
None of the cheap LatAm modules advertises an IFT/CRT certificate. **Do not assume one exists
because the datasheet says "Latin America."** A7670SA does carry FCC + Anatel, which shortens
the RF test scope without eliminating the filing.
Treat it as one fixed cost amortized over hundreds of units — which is an argument *for* the
cheap module, not against it.
---
## 10. Phases & gates
| Phase | Work | Gate |
|---|---|---|
| **−1** — 🔴 **before any of this** | **Fix the base / caster.** 10-Ago fleet truth: *zero corrections applied anywhere* → check `HelgenTestStation` @ rtk2go:2101. **No silicon in this brief produces RTK if the correction source is broken.** v6 would ship five units of new hardware into the same SPS-not-RTK result | **G0: at least one fielded unit holding GGA quality 4/5 for 1 h on the existing hardware** |
| **0** — now, parallel | **Run the T1 telemetry query (§2.4) — it is free and it decides the modem tier** · lock §1.4 GNSS · lock §3.2 rail · answer §4.4 antenna · **call a homologación lab** · order A7670SA breakout + M2M SIM (~$40 total) | Decisions in this doc, v1.1 |
| **1** — while v5.5 is in fab | **NTRIP-in-STM32 spike on a v5.5 board via the XBee socket** + A7670SA breakout: AT driver, TCP, NTRIP v1 GET, RTCM3 → GNSS UART2, reconnect/backoff, `$PHLG` signal telemetry | **G1: 24 h continuous RTK fix through the on-board modem path, ≥1 reconnect survived** |
| **2** — schematic | New blocks (GNSS island, LTE island, 3.8 V rail, MFF2, level shift, RF chains) + **all of §7** | **G2: ERC 0** + external review of the RF and power sections |
| **3** — stackup & floorplan | Confirm 50 Ω geometry against JLCPCB's calculator · place per §5 · verify GNSS↔LTE separation · confirm the enclosure takes a second bulkhead | **G3 (design): RF traces referenced to unbroken In1, no exceptions** |
| **4** — route | RF first, then power (wide from day one), then signal · zones · DRC | DRC 0 · independent netlist parse |
| **5** — DFM | BOM/CPL, LCSC pass, consignment plan, controlled-impedance order option | fab release |
| **6** — first article | Bring-up, DFU, **then G3 measured**: fix quality with modem idle vs. TX at full power | **G3 (bench): no measurable degradation in fix quality or C/N₀ under LTE TX** |
**Phase 1 is the whole trick.** It converts the largest unknown in v6 — firmware you have never
shipped talking to a radio you have never soldered — into a bench result you obtain on hardware
already paid for, weeks before committing a layout.
---
## 11. What I'd argue for, in one paragraph
Take the **A7670SA** — it is the cheapest part that is simultaneously band-correct, JLC-assemblable,
2G-capable and well-documented, and the money you save there is real ($113/unit against the
Ardusimple). But **keep the ZED-F9P for v6**. The $450 you would save at 30 units by switching
to an LG290P does not pay for a new configuration protocol, a new backend decoder, and a fresh
RTCM-compatibility unknown — in the same cycle where you are already taking on your first RF
front end, your first cellular radio and your first NTRIP client. Do the receiver cost-down as
a contained v7 respin of one corner of the board, once volume makes it worth $17k. **One new
unknown at a time** is the rule that gets this board to work on the first article.
---
## 12. Cross-references
- v5.5 EMC fix list: `2026-08-22_Drogheda_EinarBoard-v5_EMC-Audit_v1.0.md`
- Prior modem survey (bench-era): `2026-07-08_Drogheda_4G-Modem-Alternatives_v1.0.md`
- 3.0 brief & method rules: `2026-08-12_Drogheda_EinarBoard-3.0_MCU-Integration_Design-Brief_v1.0.md`
- Canonical electrical state: `watchdog-electrical-design-state.md`
- Invariants touched: **#1** power-off safe · **#2** never vanish (§2.3, §3.2) · **#3**
corrections ride the 4G, recover by power-cycling (§3.2 `MODEM_PWR_EN`)

View File

@ -0,0 +1,337 @@
---
title: EinarBoard v6 — Phase 1 — NTRIP Client in the STM32 — Bench Spec & Execution Prompt
tags: [project-drogheda, firmware, v6, ntrip, a7670sa, 4g, rtcm, bench, design-brief, prompt]
project: Project Drogheda
author: Helgen CTO + Claude
date: 2026-08-24
status: Active — THE spec for the Phase 1 bench spike. Execute before any v6 layout exists.
authority: implements Phase 1 of `2026-08-24_…EinarBoard-v6_GNSS-4G-Integration_Design-Brief_v1.0.md`.
Firmware baseline = `04-Source-Code/firmware/blackpillrtkcode_v3` (v3.0.3). Where this
conflicts with the v6 brief on Phase 1 mechanics, THIS file wins.
---
# Phase 1 — NTRIP client in the STM32 (bench spike)
> **Why this exists.** The Ardusimple 4G NTRIP Master runs the NTRIP client *on the module*.
> A bare A7670SA does not. v6 therefore requires a piece of firmware Helgen has never
> shipped, sitting directly on the correction path — the exact path the June RCA already
> proved you cannot see into. **This spec retires that risk on a $40 bench rig, before a
> single v6 trace is drawn.**
---
## 0. ⚠ Correction to the v6 brief §10
The brief said to run this spike **on a v5.5 board through its XBee socket**. After extracting
the actual v5 netlist, **that is wrong on two counts** and the plan is changed:
1. **The socket cannot power the modem.** `J7.1 = /3V3_MODEM`, generated by **U7 (MP1584)
→ L7 (10 µH/2 A) → 3V3_MODEM**, C41 22 µF + C43 100 µF. The A7670SA needs
**VBAT 3.4–4.2 V at up to 2.8 A**. Wrong voltage, wrong current, wrong rail. Feeding it
from there would brown out `SW_5V` and reproduce the 10-Ago boot loop on the bench.
2. **The UART nets collide.** `/USART1_RX` (PA10) carries **three** loads: `U16.43`,
`K1.1` (relay NC) and `MOD1.23` (the F9P's TX2). There is no free RX pin on that net for
the modem, and with K1 de-energised the NC contact ties `/USART1_RX` to `/DIN_4G`
(modem RX) — driving the modem's TX into that net creates a **self-loop**.
**Phase 1 therefore runs on a standalone bench rig** (§1) that mirrors the v6 topology exactly,
using pins that are free in v6. Nothing is soldered to the 15 boards in fab. An optional
confirmation run on a real v5.5 board is described in §9 — **after** G1 passes.
---
## 1. Bench rig
### 1.1 Bill of materials (~US$45, all obtainable in MX in days)
| Item | Note |
|---|---|
| **STM32F411CEU6 "Blackpill"** | already in stock. Same family as the F411RET6 — USART6 lives on PB6/PB7 in the 48-pin package (see §1.3) |
| **A7670SA core board** with USB, nano-SIM holder, UART header, u.FL/SMA | ~US$12–18 (MercadoLibre / AliExpress). **⚠ Confirm the variant is `-SA`.** An `-E`/`-C` board has no B4/B66 and will never attach on Telcel |
| *(fallback)* LILYGO **T-A7670SA** | ~US$33; has its own LiPo + 5 V regulation. Requires flashing the on-board ESP32 with a transparent UART bridge, or tapping the modem pins directly |
| **simpleRTK2B Pro** + multiband antenna | already owned |
| **LTE antenna** + pigtail | ~US$5 |
| **M2M SIM**, Telcel or AT&T MX, real data plan | ⚠ **not** a flat-rate sensor SIM — see v6 brief §6.4 |
| Bench PSU capable of **4.0 V / 3 A**, or a 1S LiPo | powers the modem *only* |
| **SN74LVC2T45DCUR** ×2 (or a TXS0108E breakout) | only if the core board exposes raw **1.8 V** I/O |
### 1.2 Power — the rule that keeps this honest
> **The modem gets its own supply. Never off the Blackpill's 3V3 or a USB 5 V pin.**
> 4.0 V, ≥3 A, with **≥470 µF of low-ESR bulk within 20 mm of the module's VBAT pins**, and
> **all three VBAT pins tied**. Grounds common, single star point at the modem.
>
> Instrument this: put a scope on VBAT during the first LTE attach and **record the droop**.
> **>300 mV means the v6 rail is undersized** and you have learned it for $0. This measurement
> is a Phase 1 deliverable, not an optional extra — it is the empirical check on the
> `AP64350`-class buck chosen in v6 Block P.
### 1.3 Wiring — mirrors v6 exactly
| Function | v6 net | v6 MCU pin | Bench (Blackpill F411CE) |
|---|---|---|---|
| MCU → modem UART (AT + GGA + `$PHLG`) | `/USART6_TX` | **PC6** | **PB6** (USART1_TX, AF7) |
| modem → MCU UART (responses + RTCM tap) | `/USART6_RX` | **PC7** | **PB7** (USART1_RX, AF7) |
| modem UART TX → GNSS UART2 RX (RTCM, hardware path) | `/DOUT_4G` | — | direct wire to the Pro's **RX2** |
| GNSS UART1 ↔ MCU | `/GNSS_TX`, `/GNSS_RX` | PA2 / PA3 | PA2 / PA3 (unchanged, `HardwareSerial GNSS(PA3, PA2)`) |
| GNSS UART2 TX (uplink NMEA tap) | `/USART1_RX` | PA10 | **not wired on the bench** — see the note below |
| modem PWRKEY | `/MODEM_PWRKEY` | **PC8** | **PB8**, via 2N7002 open-drain |
| modem STATUS | `/MODEM_STATUS` | **PC9** | **PB9**, level-shifted |
| modem RESET | `/MODEM_RST` | **PC11** | **PB5**, via 2N7002 open-drain |
| modem rail enable | `/MODEM_PWR_EN` | **PC10** | **PB4** → bench PSU inhibit / load-switch |
> ⚠ **The 48-pin Blackpill has only two usable UARTs, and this matters.** On the F411**CE**
> (UFQFPN48) there is no PC6/PC7, so USART6 is only available on PA11/PA12 — **the USB pins**.
> That leaves USART2 (PA2/PA3, the receiver) and USART1 (PA9/PA10 **or** PB6/PB7, the modem).
> Remapping USART1 to PB6/PB7 for the modem means **PA10 cannot simultaneously be
> USART1_RX**, so the UART2 uplink tap is not available on the bench rig.
>
> **This costs nothing.** The MCU already receives the full NMEA stream — GGA included — from
> the receiver's UART1 on USART2. Phase 1 sources the caster GGA from there. The PA10 tap
> exists on v6 only to relay the receiver's UART2 output byte-for-byte for Modo B, which is a
> v6 concern, not a Phase 1 one. **The F411RET6 on v6 has PC6/PC7 and keeps all three.**
> **Level shifting.** A7670SA I/O is **1.8 V**. `MODEM_TX` needs 3.3 → 1.8; `MODEM_RX` and
> `MODEM_STATUS` need 1.8 → 3.3 (the STM32's V_IH ≈ 2.31 V — **1.8 V will not read as HIGH**).
> `PWRKEY` and `RESET` have internal 50 kΩ pull-ups to VBAT: drive them **open-drain** with a
> 2N7002, never push-pull, so an absent MCU leaves them unasserted.
> Many A7670SA core boards already carry a shifter — **verify with a meter before connecting
> anything**, and never drive 3.3 V into a raw 1.8 V pin.
### 1.4 Topology note — the fail-safe survives
On v6 the modem's UART TX fans out to **both** the GNSS UART2 RX (hardware RTCM path) **and**
the MCU. K1 keeps its role: **NC = F9P TX2 → modem RX** (MCU dead ⇒ the receiver's own GGA
still reaches the caster ⇒ corrections keep flowing), **NO = PC6** (MCU in line, forwarding GGA
and injecting `$PHLG`). The only schematic change is that **K1 pin 10 moves from PA9 to PC6**
(v6 Block T). Replicate this on the bench with a jumper: for Phase 1, run permanently in the
"MCU in line" position.
**Consequence, stated plainly: v6 makes Modo B the normal operating mode, not a bench special.**
That is consistent with the EPCT decision already on record (`$PHLG` in-band via Modo B).
---
## 2. Firmware architecture
### 2.1 Placement and guards
New block in `blackpillrtkcode_v3.ino` (Phase 1) → promoted to `blackpillrtkcode_v4/` for v6.
Follow the file's existing idiom exactly:
```c
#define ENABLE_NTRIP_CLIENT 0 // 0 = fleet (modem owns NTRIP) · 1 = MCU owns NTRIP [v6]
#define ENABLE_NTRIP_DEBUG 1 // NTRIP/AT trace lines on the USB STREAM log
```
`#ifndef`-guarded so `-DENABLE_NTRIP_CLIENT=1` works as an `arduino-cli` build flag without
editing the file — same pattern as `ENABLE_MODOB_BENCH`. FW version string becomes
**`"3.1.0-ntrip"`** so a stray flash is detectable in `$PHLG` field 1.
### 2.2 Hard rules
1. **No `delay()`. No blocking reads.** Everything is a millis()-timed state machine serviced
from `loop()`, like `chargeGovernor()` and `modoBService()`. The IWDG is on
(`ENABLE_IWDG 1`) — a blocking AT wait will reset the board.
2. **No `String`, no heap.** Fixed buffers, `snprintf`, exactly as `buildPHLG()` does.
3. **The NTRIP client never gates the safety loop.** Power, charge governor, thermal and fan
services keep running at their existing cadence regardless of modem state.
4. **Secrets never enter git** (invariant #6). APN and caster credentials come from
`config/ntrip.local.h` (git-ignored) or `-D` build flags. Ship
`config/ntrip.example.h` alongside the existing `4G_NTRIP_HelgenSRV-NMEA.example.txt`.
### 2.3 State machine
```
┌──────────────────────────── beginBackoff(reason) ────────────────────┐
│ │
MODEM_OFF ─► RAIL_UP ─► PWRKEY_PULSE ─► WAIT_READY ─► CONFIG ─► ATTACH ─► │
▲ │ │
│ ▼ │
│ SOCKET_OPEN ─► NTRIP_GET ──────┤
│ │ │
│ ▼ │
└──────────────── HARD_CYCLE ◄── BACKOFF ◄──────────────── STREAMING ──┘
```
| State | Action | Timeout → next |
|---|---|---|
| `MODEM_OFF` | `MODEM_PWR_EN` low; all modem GPIO released | — |
| `RAIL_UP` | `MODEM_PWR_EN` high | 100 ms → `PWRKEY_PULSE` |
| `PWRKEY_PULSE` | PWRKEY low **≥ 50 ms** *(SIMCom; **never** copy Quectel's ≥2 s)*, release | → `WAIT_READY` |
| `WAIT_READY` | wait for `RDY`/`+CPIN: READY`; UART ready ≈ 8 s, STATUS asserts ≈ 11.2 s | **20 s** → `BACKOFF` |
| `CONFIG` | `ATE0` · `AT+CMEE=2` · `AT+CPIN?` · `AT+CGDCONT=1,"IP","<APN>"` · `AT+CSQ` · `AT+CREG?` | 10 s → `BACKOFF` |
| `ATTACH` | `AT+CGATT?` poll until `1`; then `AT+NETOPEN` | **90 s** → `BACKOFF` |
| `SOCKET_OPEN` | `AT+CIPOPEN=0,"TCP","<caster>",<port>` | 30 s → `BACKOFF` |
| `NTRIP_GET` | send the §3 request; await `ICY 200 OK` / `HTTP/1.x 200` | 15 s → `BACKOFF` |
| `STREAMING` | transparent RTCM in; GGA + `$PHLG` out; watchdogs of §5 | see §5 |
| `BACKOFF` | rail stays up, socket closed, exponential wait | → `PWRKEY_PULSE` |
| `HARD_CYCLE` | `MODEM_PWR_EN` low **≥ 2 s**, then `MODEM_OFF` | invariant #3's recovery path |
**Backoff ladder (mirror `chargeGovernor()`'s cooldown idiom):**
`5 s → 10 → 20 → 40 → 80 → 160 → 300 s cap`. **Reset to 5 s after 10 minutes of healthy
`STREAMING`.** After **3 consecutive** `BACKOFF` entries without ever reaching `STREAMING`,
escalate to `HARD_CYCLE`.
> **Invariant #2 — never vanish, never loop.** The backoff ladder plus the 3-strike escalation
> is what stops this becoming a fast-reboot loop. It is not decoration; it is the invariant.
---
## 3. NTRIP v1 handshake — exact bytes
```
GET /<MOUNTPOINT> HTTP/1.0\r\n
User-Agent: NTRIP HelgenHBOX/3.1\r\n
Authorization: Basic <base64(user:pass)>\r\n
Accept: */*\r\n
Connection: close\r\n
\r\n
```
- Accept **either** `ICY 200 OK` (NTRIP v1) or `HTTP/1.0 200 OK` / `HTTP/1.1 200 OK`.
- `401` → **do not retry blind.** Latch `NTRIP_AUTH_FAIL`, emit an `EVT`, and back off to the
300 s cap. Hammering a caster on bad credentials gets you IP-banned — and rtk2go does ban.
- `SOURCETABLE 200 OK` means **the mountpoint does not exist**. Distinct latch, distinct EVT.
This is a real failure mode and the June RCA would have caught the caster problem months
earlier if the firmware had reported it.
- Base64 is computed **at build time** from the credentials header — no runtime encoder, no
credentials in RAM as plaintext longer than necessary.
**GGA uplink.** Send one `$GPGGA`/`$GNGGA` to the caster **every 10 s** while `STREAMING`,
**byte-exact, never rebuilt** — copy the sentence verbatim from the receiver's own output
(bench: the USART2 NMEA stream; v6: the PA10 UART2 tap). VRS mountpoints require it;
single-base mountpoints ignore it harmlessly. A rebuilt GGA with a recomputed checksum is the
classic way to get silently dropped by a caster — `nmeaChecksumOk()` already exists in the
firmware for validating it, but the bytes sent must be the originals.
---
## 4. RTCM handling and the telemetry that closes the June RCA
RTCM3 bytes flow **modem → GNSS UART2 in hardware**. The MCU sees the same bytes on its RX and
**parses them for telemetry without being in the data path's critical timing**.
Frame walker (no buffering of payloads — count only):
- Preamble `0xD3`, then 6 reserved bits + **10-bit big-endian length**, then payload, then
**3-byte CRC-24Q**.
- Message type = first 12 bits of the payload.
- Maintain: `rtcmBytes` (u32), `rtcmMsgs` (u32), `lastRtcmMs`, `rtcmCrcBad` (u16), and a
small bitmap of **seen message types** (1005/1006 station, 1074–1077 GPS MSM,
1084–1087 GLO, 1094–1097 GAL, 1124–1127 BDS).
> **This is the single most valuable thing Phase 1 produces.** On 10-Ago the fleet showed
> *zero corrections applied anywhere* and there was no way to tell whether the caster, the
> modem, the wiring or the receiver was at fault. After this, `$PHLG` answers it directly:
> RTCM arriving but no fix ⇒ receiver/config or base-band mismatch. No RTCM ⇒ caster or link.
> **Build the observability before you build the board.**
**`STREAMING` health watchdogs:**
| Condition | Action |
|---|---|
| no RTCM byte for **30 s** | → `BACKOFF` ("rtcm stall") |
| no TCP data at all for **60 s** | → `BACKOFF` |
| `+CIPCLOSE`/`+IPCLOSE` URC | → `BACKOFF` immediately |
| `CSQ` = 99 for **60 s** | → `BACKOFF` |
| 3 backoffs, never streamed | → `HARD_CYCLE` |
---
## 5. `$PHLG` extension — append-only
`buildPHLG()` is documented append-only and the backend decoder is already deployed. v3.0.3
emits 25 fields. **Append six, in this order**, and change nothing before them:
| # | Field | Type | Meaning |
|---|---|---|---|
| 26 | `ntrip` | u8 | modem state enum (0=OFF … 8=STREAMING, 9=AUTH_FAIL, 10=NO_MOUNT) |
| 27 | `rtcm_age` | u32 | ms since last RTCM byte (`0xFFFFFFFF` = never) |
| 28 | `rtcm_msgs` | u32 | RTCM3 frames accepted this power session |
| 29 | `rtcm_bad` | u16 | CRC-24Q failures |
| 30 | `csq` | u8 | last `AT+CSQ` RSSI index (99 = unknown) |
| 31 | `nret` | u16 | backoff entries this power session |
Also extend the `COLS,` header in `logBoot()` and the `TLM,` record in `logTelemetry()` with the
same six, same order. **Backend:** `retranslator`'s `$PHLG` decoder must tolerate both 25- and
31-field sentences — mixed-firmware fleets are guaranteed during rollout.
Update `docs/LOGGING-SPEC.md` to **v2.2** in the same commit.
---
## 6. hostsim
`tools/hostsim` already carries `sim`, `sim_mb`, `rollover` and a `run.sh` driver. Add:
- **scenario `ntrip`** — a fake modem behind a pipe that can be scripted to: attach normally ·
never attach · return `401` · return `SOURCETABLE` · accept then go silent mid-stream ·
drop TCP · emit malformed RTCM (bad CRC).
- Assertions: the ladder is exponential and capped; it **resets after 10 min healthy**;
`HARD_CYCLE` fires on the 3rd fruitless backoff and **not** before; `$PHLG` stays ≤ 160 bytes
with all 31 fields; the XOR checksum is right; **no path ever calls `delay()`**.
- `run.sh` builds and runs both `ENABLE_NTRIP_CLIENT` variants, as it already does for `modob`.
> Green hostsim is a **precondition** for touching the bench rig, not a post-hoc check.
---
## 7. G1 — acceptance gate
**G1 passes only when all of these hold on the bench rig, in one unbroken run:**
1. **24 h continuous**, `fixQuality ≥ 4` (RTK-fixed or float) for **≥ 95 %** of samples.
2. **≥ 1 forced reconnect survived** — pull the LTE antenna for 60 s, restore. RTK must return
**without human intervention** and `nret` must increment.
3. **≥ 1 `HARD_CYCLE` exercised** — hold the modem in a fruitless state 3 backoffs running;
confirm the rail drops ≥2 s and the unit recovers.
4. **`rtcm_msgs` monotonically increasing** and the message-type bitmap shows the expected
MSM sets for the base in use.
5. **VBAT droop < 300 mV** at LTE attach, scope-captured (§1.2), with the capture archived to
`06-Testing-Validation/`.
6. **No IWDG reset** across the whole run (`g_resetCause` stays as booted).
7. Auth/mountpoint failures produce the **correct distinct latches**, verified by deliberately
using bad credentials and a bad mountpoint.
**Deliverables:** the 24 h `TLM` log, the scope capture, the hostsim run output, and a one-page
result note in `06-Testing-Validation/`.
---
## 8. ⚠ Phase 1 depends on Phase −1
**Gate G0 first.** The 10-Ago fleet truth is *zero corrections applied anywhere* → check
`HelgenTestStation` @ rtk2go:2101. If the caster or base is broken, this bench rig will
faithfully report `STREAMING` with `rtcm_msgs = 0` and you will spend a week debugging
firmware that is working correctly. **Prove the correction source on existing hardware
before starting.**
---
## 9. Optional confirmation on a real v5.5 board (after G1)
Only once G1 has passed, and only if you want the extra confidence:
1. **Remove the simpleRTK2B Pro shield** from MOD1 — otherwise the F9P's TX2 contends with the
modem on `/USART1_RX`.
2. **Energise K1** (`PIN_BYP_CTRL` HIGH) so COM–NO is closed and `/DIN_4G` is isolated from
`/USART1_RX`.
3. Solder to the **THT relay pins** (accessible, no fine-pitch work): **K1 pin 10 = PA9**,
**K1 pin 1 = PA10**, **K1 pins 5/6 = `/DIN_4G`**. `/DOUT_4G` is available at **J7 pin 2**.
4. The modem still runs from its **own** 4.0 V supply. `3V3_MODEM` stays unloaded.
This confirms the connector and relay mechanics only. It does **not** validate the v6 pin map
(PC6/PC7 have no copper on v5.5) — that is what Phase 6 first-article bring-up is for.
---
## 10. Cross-references
- v6 brief: `2026-08-24_Drogheda_EinarBoard-v6_GNSS-4G-Integration_Design-Brief_v1.0.md`
- Schematic delta: `2026-08-24_Drogheda_EinarBoard-v6_Phase-2_Schematic-Delta_v1.0.md`
- Firmware baseline: `04-Source-Code/firmware/blackpillrtkcode_v3/` (v3.0.3), `docs/LOGGING-SPEC.md`
- Modo B / `$PHLG` decision: `2026-08-08_Drogheda_Firmware-v3.1_ModoB-PHLG_Design-Brief_v1.0.md`
- Ardusimple config being replaced: `04-Source-Code/firmware/config/4G_NTRIP_HelgenSRV-NMEA.example.txt`
- Invariants: **#1** power-off safe · **#2** never vanish, never loop (§2.3 ladder) ·
**#3** corrections ride the 4G, recover by power-cycling (`HARD_CYCLE`) · **#6** no secrets (§2.2)

View File

@ -0,0 +1,477 @@
---
title: EinarBoard v6 — Phase 2 — Schematic Delta (v5 → v6) — Build Spec
tags: [project-drogheda, hardware, einarboard, v6, schematic, kicad, gnss, 4g, rf, delta]
project: Project Drogheda
author: Helgen CTO + Claude
date: 2026-08-24
status: Active — THE schematic spec for v6. Every net/pin below is verified against the
as-drawn `hbox-watchdog-v5.kicad_pcb` netlist (extracted 2026-08-24), not assumed.
authority: implements Phase 2 of `2026-08-24_…EinarBoard-v6_GNSS-4G-Integration_Design-Brief_v1.0.md`.
Supplements `watchdog-electrical-design-state.md` (canonical) and
`watchdog-production-schematic-delta.md` (the v3 precedent for this document's format).
---
# EinarBoard v6 — Schematic Delta
> **Base:** `kicad/hbox-watchdog-v5` (EinarBoard 3.0 / v5.5, 15 pcs in fab since 22-Ago,
> git checkpoint `c3e8e48`). **Target:** `kicad/hbox-watchdog-v6`.
>
> **Method rules, unchanged and non-negotiable:** file-level KiCad edits only (the MCP backend
> clobbered disk state on 24-Jul — read-only MCP queries are fine); git checkpoint after every
> block; every structural edit verified by an independent parse; `%.10g` coordinates; footprint
> rotation = R(−θ); zone refills in the GUI only.
>
> ⚠ **Before starting:** clear the stale `.git/*.lock` files and confirm the v5.5 fix commit
> actually landed. This has bitten two sessions running.
---
## 0. ⚠ Two corrections to the v6 brief, from the extracted netlist
The brief was written before the netlist was pulled. Both of these were wrong:
1. **"Spare PCF8574 pins" — there is only ONE.** `U13` (PCF8574T, SOIC-16 **WIDE**) has
P0–P3 on `/BATLED1–4`, P4–P6 on `/RGB_R/G/B`. **Only `P7` (pin 12) and `~INT` (pin 13)
are free.** All new control signals therefore go on **MCU pins**, of which there are
plenty (§3).
2. **`/MODEM_RST` is not MCU-controlled today.** It connects only `J7.5` and `SW4G1.1`
(a pushbutton). The firmware has **never** been able to reset the modem in software — its
only tool is killing `SW_5V` via `PWR_CTRL`. **v6 fixes this** (Block T).
A third finding worth recording: **`/3V3_MODEM` is made by `U7` (MP1584) → `L7` → `3V3_MODEM`**,
input `/SW_5V`. `U7`'s job changes completely in v6 (§Block P).
---
## 1. Verified v5 baseline (extracted 2026-08-24)
### 1.1 STM32F411RET6 (`U16`) — as drawn
| Pin | Port | Net | Pin | Port | Net |
|---|---|---|---|---|---|
| 2 | PC13 | `/LED_C13` | 41 | PA8 | `/CHRG_EN` |
| 8 | PC0 | `/ETH_INT` | 42 | PA9 | `/USART1_TX` |
| 14 | PA0 | `/BAT_SENSE` | 43 | PA10 | `/USART1_RX` |
| 15 | PA1 | `/PWR_CTRL` | 44/45 | PA11/12 | `/DFU_DM` `/DFU_DP` |
| 16 | PA2 | `/GNSS_TX` | 46/49 | PA13/14 | `/SWDIO` `/SWCLK` |
| 17 | PA3 | `/GNSS_RX` | 50 | PA15 | `/ETH_RST` |
| 20 | PA4 | `/5V_SENSE` | 55 | PB3 | `/CHRG_STAT` |
| 21 | PA5 | `/IGN_SENSE` | 56 | PB4 | `/CHRG_FAULT` |
| 22 | PA6 | `/FAN_PWM` | 57 | PB5 | `/BYP_CTRL` |
| 26 | PB0 | `/EXT_LED` | 58/59 | PB6/PB7 | `/I2C_SCL` `/I2C_SDA` |
| 27 | PB1 | `/NTC_TEMP` | 61 | PB8 | `/SER_IRQ` |
| 28 | PB2 | `/BOOT1_STRAP` | 33–36 | PB12–15 | `/ETH_CS/SCK/MISO/MOSI` |
**Free (unconnected in v5): PC1, PC2, PC3, PC4, PC5, PC6, PC7, PC8, PC9, PC10, PC11, PC12,
PD2, PA7, PB9, PB10** — 16 pins. **USART6 = PC6/PC7 is free**, which is what makes v6 possible
without disturbing anything.
### 1.2 The three nets that carry the whole GNSS/4G architecture
| Net | v5 members | Meaning |
|---|---|---|
| `/USART1_RX` | `U16.43` (PA10) · `Rdfu1.1` (22k) · **`K1.1` (NC)** · **`MOD1.23`** (F9P TX2) | receiver's UART2 uplink; MCU taps it passively |
| `/DIN_4G` | **`K1.5/6` (COM)** · `J7.3` | modem's data-**in**; fed from the relay |
| `/DOUT_4G` | `J7.2` · **`MOD1.17`** (F9P RX2) | **RTCM downlink, hardwired, never switched** |
`K1` (G5V-1 DC5): `1`=NC=`/USART1_RX` · `5/6`=COM=`/DIN_4G` · `10`=NO=`/USART1_TX` (PA9) ·
`2`=`/SYS5V` · `9`=`/KCOIL` ← `BYP_CTRL` (PB5) via `R40`→`Q6`.
### 1.3 Power tree (verified)
```
P1.1 ─/24V_IN─┬─ D10 (SMBJ33A)
└─ Q10 (IRFR5305, rev-pol) ──/24V── U2.7 (MP1584 fan buck) ──/SW_FAN─► 5V_FAN
P2.1 ─/VEH5V── RS1 (8 mΩ) ──/VIN_SW── U6.7 (LTC4040 VIN)
U6 (LTC4040) ──/MAIN5V── Q7 (AO4406A cutoff, gate /IGATE) ──/SYS5V── F1 (PPTC 3A)
├─ U12 (MP1584) ─► /3V3_ETH
└─ K1.2 (relay coil supply)
/SW_IN ── Q1 (AO3401A, gate /GATE_LS ← PWR_CTRL) ──/SW_5V──┬─ MOD1.5 (shield 5 V)
└─ U7.2/7 (MP1584) ─L7─► /3V3_MODEM
U17 (AMS1117-3.3) ──/3V3
```
---
## 2. Deletions
| Ref | What | Action |
|---|---|---|
| `MOD1` | simpleRTK2B Pro, Arduino UNO R3 socket, 32 pads @ (90.6, 129.9) | **DELETE** — nets `/GNSS_RX` `/GNSS_TX` `/DOUT_4G` `/USART1_RX` `/SW_5V` re-terminate on `U18` |
| `J7`, `J8` | XBee 2.0 mm sockets, 10 pads each @ (174.2 / 196.5, 74.8) | **DELETE** — nets re-terminate on `U19` |
| `SW4G1` | modem RESET pushbutton | **DELETE** — replaced by MCU control (Block T) |
| `Rmled1`, `MODEM_LED_A` | modem LED + resistor | **KEEP**, re-source from the A7670SA `NETLIGHT` pin |
| `MH7`, `MH9`, `MH10`, `MH11` | shield / heatsink M2.5 bolt holes | **DELETE MH7/MH9/MH10**; **KEEP MH11** and add one partner near `U19` for the modem heatsink clamp |
| `U7`, `L7`, `C41`, `C43` | the 3V3 modem buck | **REPURPOSE** — see Block P |
**Do not delete `K1`.** It is rewired (Block T), not removed — it is the fail-safe that keeps
corrections flowing with a dead MCU.
---
## 3. MCU pin allocation — v6 additions
| New net | Pin | Dir | Function |
|---|---|---|---|
| `/USART6_TX` | **PC6** | OUT | MCU → modem (AT, GGA uplink, `$PHLG`) — **via `K1` NO** |
| `/USART6_RX` | **PC7** | IN | modem → MCU (AT responses, RTCM tap) |
| `/MODEM_PWRKEY` | **PC8** | OUT | open-drain via `Q20` (2N7002) |
| `/MODEM_STATUS` | **PC9** | IN | level-shifted 1.8 → 3.3 V |
| `/MODEM_PWR_EN` | **PC10** | OUT | enables `U20` buck — the `HARD_CYCLE` path |
| `/MODEM_RST` | **PC11** | OUT | open-drain via `Q21` (2N7002) — **new MCU control** |
| `/GNSS_RESET_N` | **PC12** | OUT | open-drain; `U18` needs a 7–13 kΩ pull-up |
| `/GNSS_ANT_DETECT` | **PC1** | IN | antenna-open telemetry |
| `/GNSS_ANT_SHORT_N` | **PC2** | IN | antenna-short telemetry |
| `/GNSS_TIMEPULSE` | **PA7** | IN | TIM3_CH2 input capture |
**Unchanged:** `/GNSS_TX` PA2 · `/GNSS_RX` PA3 (USART2, `HardwareSerial GNSS(PA3, PA2)`) ·
`/USART1_RX` PA10 (receiver UART2 tap) · `/BYP_CTRL` PB5 (K1 coil).
**Freed:** **PA9** — `K1.10` moves to PC6. Leave PA9 as a test point.
**Still spare after v6:** PC3, PC4, PC5, PB9, PB10, PD2, PA9 — **7 pins.**
> ⚠ **Verify no double-allocation** with an independent netlist parse before ERC. That check
> is the reason this table exists.
---
## Block N — GNSS island (`U18`)
**Part:** `ZED-F9P-04B`, 54-pin LGA 22.0 × 17.0 × 2.4 mm — **LCSC `C5120257`, Extended.**
*(Alternative under evaluation for v7: LG290P, 12.2 × 16 mm — see brief §1.4. **Do not**
attempt a dual footprint; the pad rings overlap and the pin functions do not align.)*
| Signal | Connect to | Note |
|---|---|---|
| `VCC` | `/3V3_GNSS` | **from `U21` LDO only** — never the shared `/3V3` |
| `V_BCKP` | `/3V3` via 1 kΩ + Schottky | 45 µA hot-start; keeps ephemeris across a power-cycle |
| `GND` | GND | full pad-under-module via field |
| `UART1 TX/RX` | `/GNSS_RX` / `/GNSS_TX` | ⚠ net names are **MCU-relative** in this design: `/GNSS_TX` = MCU PA2 out → receiver RX |
| `UART2 RX` | `/DOUT_4G` | **RTCM in — hardware path, unswitched** |
| `UART2 TX` | `/USART1_RX` | uplink NMEA → `K1.1` (NC) and PA10 |
| `RESET_N` | `/GNSS_RESET_N` | **7–13 kΩ pull-up to `/3V3_GNSS` required** |
| `TIMEPULSE` | `/GNSS_TIMEPULSE` | 4 mA drive |
| `ANT_DETECT` / `ANT_SHORT_N` | `/GNSS_ANT_DETECT` / `/GNSS_ANT_SHORT_N` | free antenna-fault telemetry — you have already lost H0005 and H0014 to antenna faults |
| `VCC_RF` | Block S bias tee | 50 mA typ / **300 mA abs max** |
| `D_SEL` | leave open (UART mode) | open/high = UART; low = SPI |
| `SAFEBOOT_N` | leave open | |
| `USB` (`V_USB`, D+/D−) | **test pads only**, not a connector | u-center access at first article; 90 Ω pair, ESD if ever fitted |
**Decoupling:** 100 nF per supply pin **≤ 2 mm**, 10 µF + 1 µF bulk ≤ 10 mm, per the
Capacitor-Placement-Map rules.
### `U21` — GNSS LDO
**3.3 V, ≥ 300 mA, low noise.** Candidates: `TPS7A2033PDBVR` (low-noise, preferred) or
`AP2112K-3.3TRG1` (cheap, LCSC `C51118`).
**Input = `/SW_5V`, output = `/3V3_GNSS`** — deliberately *not* the shared `/3V3`. Two reasons:
the receiver stays on the MCU-gated stack rail, so `PWR_CTRL` (PA1 → `Q1`) still power-cycles
it exactly as it does today; and an LDO fed from 5 V has ~1.7 V of headroom, which is what
gives it the PSRR to reject the three MP1584 switchers this board carries. The ZED-F9P has
**no internal SAW** — it will lock onto buck harmonics given the chance.
> 🔴 **`ZED-F9P` VCC ramp rate ≤ 20 µs/V is a DAMAGE limit, not a guideline** — 0 → 3.3 V in
> **≤ 66 µs**. Most LDOs with soft-start are far slower. **Measure the chosen LDO's rise time
> on the bench before fab.** If it cannot meet it, add a fast load switch or a supervisor.
> This is a fab gate, not a nice-to-have.
---
## Block O — LTE island (`U19`)
**Part:** `A7670SA-LASC`, 88-pin LGA 24 × 24 × 2.5 mm — **LCSC `C18548276`**, in the JLCPCB
library, Extended, **X-ray inspection required, MSL 3**.
⚠ **`-SA` only.** `-E`/`-C` variants have no B4/B66 and will never attach on Telcel.
⚠ A7670 (88-LGA) ≠ A7672 (124 LCC+LGA) ≠ SIM7070 (68-LCC). Not interchangeable.
| Signal | Connect to | Note |
|---|---|---|
| `VBAT` **pins 55, 56, 57** | `/VBAT_MODEM` | **all three, mandatory** for RF performance. Trace/pour **≥ 3 mm** |
| `GND` | GND | full via field |
| `UART_TXD` | `/DOUT_4G` (level-shifted, Block R) | → `U18` UART2 RX **and** → `/USART6_RX` |
| `UART_RXD` | `/DIN_4G` (level-shifted) | ← `K1` COM |
| `PWRKEY` | `/MODEM_PWRKEY_M` | via `Q20` open-drain. **Low ≥ 50 ms = on; ≥ 2.5 s = off.** Internal 50 kΩ to VBAT. **≤ 100 nF total capacitance** or you get spurious auto-power-on |
| `RESET` | `/MODEM_RST_M` | via `Q21` open-drain, low 2–2.5 s. Same ≤ 100 nF rule |
| `STATUS` | `/MODEM_STATUS_M` | 1.8 V out; asserts ≈ 11.2 s after power-on |
| `NETLIGHT` | `Rmled1` → `/MODEM_LED_A` | reuse the existing LED |
| `VDD_1V8` | `/1V8_MODEM` + 2.2 µF | **50 mA max — level-shift reference ONLY, never a system rail** |
| `USIM1_*` | Block Q | internal 4.7 kΩ pull-up on `USIM1_DATA` — no external needed |
| `USB` D+/D−, VBUS | `J9` (kept) | 90 Ω pair. **Reserve a `USB_BOOT` test pad** — the only recovery if a firmware update bricks it. `USB_BOOT` must **not** be pulled low at power-up |
| `RF_ANT` (pin 60) | Block S | **single antenna** — Cat-1bis, no diversity port |
**Decoupling at the VBAT pins:** 33 pF ∥ 10 pF ∥ 100 nF ∥ 1 µF ceramics, plus the Block P bulk.
---
## Block P — modem 3.8 V rail (`U20`) — replaces `U7`'s modem duty
> **Why this block exists, in one sentence:** the 10-Ago fleet boot-loop was caused by
> exceeding the SD-15B's 3 A, and putting a 2 A LTE modem back on that same rail would
> recreate it.
```
/24V ──────────► D20 (SS34) ──┐
├──/VIN_MODEM──► U20 (buck, 3.8 V) ──L20──► /VBAT_MODEM ──► U19
/MAIN5V ──► D21 (PMEG3050) ───┘ ▲
│ EN
/MODEM_PWR_EN (PC10)
```
| Item | Spec |
|---|---|
| `U20` | **4.5–40 V in, ≥ 3 A, synchronous buck.** Candidate: `AP64350` class. ⚠ **40 V rating required** — `D10` (SMBJ33A) clamps at ~53 V, a 28 V part such as TPS54331 is **not** acceptable. **NOT a fourth MP1584** — the existing three are the source of EMC findings 3 and 4 |
| Output | **3.8 V** (mid-window of the A7670SA's 3.4–4.2 V) |
| `D20` | SS34 — already in the BOM (`D2`, `D12`). Drop irrelevant on the 24 V leg |
| `D21` | **low-Vf Schottky** (PMEG3050 class, Vf ≈ 0.35 V @ 2 A). In blackout `MAIN5V` ≈ 5 V → `VIN_MODEM` ≈ 4.65 V → duty ≈ 0.82. **Verify `U20`'s max duty covers it**; if marginal, use a P-FET ideal diode (`AO3401A`, already in the BOM as `Q1`) |
| Bulk | **≥ 470 µF low-ESR** within 20 mm of the VBAT pins (spec floor is 300 µF; the extra is cheap insurance against the 2.8 A GSM burst) |
| `Cin` | **1 µF + 10 µF within 3 mm** of `U20`'s VIN-GND — EMC finding 4, mandatory |
| Feedback | divider **at the FB pin**, `/FB_MODEM` short and routed away from `/SW_MODEM` — EMC finding 3, mandatory |
| Enable | `/MODEM_PWR_EN` (PC10). **Power-cycle = EN low ≥ 2 s.** Add a 1 kΩ bleed across the bulk so it actually collapses |
**Behaviour:** cabinet live → modem's ~9.5 W comes from **24 V at ~0.45 A** and the SD-15B
never sees it. Blackout → same buck accepts `MAIN5V` (pack via LTC4040), unit keeps reporting
(**invariant #2**).
### `U7` / `L7` / `C41` / `C43` — disposition
`/3V3_MODEM` no longer feeds a modem. **Repurpose `U7` as the 3.3 V supply for the level
shifters and the LTE-island logic**, renaming the net `/3V3_LTE`, **or** delete `U7`/`L7`/
`C41`/`C43` entirely and take that logic from `/3V3`. **Decide before routing the 3V3 tree** —
this is the same open decision that was left dangling on `U12`/`L8` in the 3.0 brief, and it
should not be left dangling twice. Recommendation: **delete**, feed from `/3V3`; the shifters
draw milliamps.
### 🔴 Also fix in this block
**The LTC4040 input-limit errata.** `RS1` (8 mΩ) sets a 3.12 A input limit **above** the
SD-15B-05's 3 A rating. Bring the limit **below** the PSU rating, or specify a larger PSU.
This has been a standing errata since 10-Ago. **v6 is where it gets fixed.**
---
## Block Q — MFF2 SIM (`U22`)
| Item | Spec |
|---|---|
| Package | **MFF2 / DFN-8 5 × 6 mm (SON-8)** |
| Signals | `USIM1_VDD` · `USIM1_CLK` · `USIM1_DATA` · `USIM1_RST` · GND |
| Pull-ups | none — `U19` has an internal 4.7 kΩ on `USIM1_DATA` |
| Voltage | 1.8 V / 3.0 V auto-detect, no strapping |
| ESD | array at the SIM lines (`ESDA6V1W5` / `SMF15C` class) |
| Placement | **≤ 20 mm from `U19`**, short matched-ish traces, GND guard |
⚠ **Sourcing is a lead-time item, not an engineering task.** You need a real Telcel/AT&T MX or
Mexican-profile MVNO MFF2. Flat-rate sensor IoT SIMs (500 MB / 10 years) are **an order of
magnitude too small** for 50–200 MB/month. Start this in Phase 0.
---
## Block R — level translation (`U23`, `U24`)
**A7670SA I/O is 1.8 V. The STM32's V_IH ≈ 2.31 V — 1.8 V will not read as HIGH.**
| Line | Direction | Method |
|---|---|---|
| `/DIN_4G` (MCU/K1 → modem RX) | 3.3 → 1.8 | `SN74LVC2T45DCUR`, DIR fixed |
| `/DOUT_4G` (modem TX → MCU + receiver) | 1.8 → 3.3 | `SN74LVC2T45DCUR`, DIR fixed |
| `/MODEM_STATUS` | 1.8 → 3.3 | same device, 2nd channel |
| `/MODEM_PWRKEY`, `/MODEM_RST` | 3.3 → open-drain | `Q20`, `Q21` = 2N7002. **Not** through a shifter — MCU absent must leave them unasserted |
- `VccA` = `/1V8_MODEM` (from `U19`'s `VDD_1V8`, 2.2 µF); `VccB` = `/3V3`.
- **Prefer direction-fixed `74LVC2T45` over auto-direction `TXS0108E`** — auto-direction parts
misbehave against strong drivers and multi-load nets, and `/DOUT_4G` drives **two** loads.
- ⚠ **The RTCM path passes through `U24`.** Add DNP 0 Ω bypass footprints across it so a
suspect shifter can be jumpered out at bring-up.
- `/DOUT_4G` fan-out on the 3.3 V side: `U18` UART2 RX **and** `/USART6_RX` (PC7) through a
**100 Ω series** resistor for isolation.
---
## Block S — RF front ends
### Stackup (unchanged topology, new requirement)
`F.Cu` signal + GND pour · `In1` **solid GND** · `In2` 3V3 plane · `B.Cu` signal + GND pour. ✔
**New: order as controlled impedance.** JLCPCB 4-layer `JLC04161H-7628`, F.Cu→In1 prepreg
≈ 0.2104 mm. Target **50 Ω CPWG on F.Cu referenced to In1** — roughly 0.35 mm trace / 0.25 mm
gap at εr ≈ 4.4. **Confirm with JLCPCB's own impedance calculator before layout, not after.**
**Rules for both RF traces, no exceptions:** referenced to an unbroken `In1` (no plane split,
no `In2` island underneath), as short and straight as possible, ground-stitched both sides at
≤ λ/20, nothing crossing on any layer.
### S1 — GNSS (`J12`)
```
J12 (U.FL) ─╥─ C_DC (100 nF) ─── U18.RF_IN
║
└─ L_BIAS (≥68 nH 0603, SRF > 1.6 GHz) ─┬─ 100 nF ∥ 100 pF ─ GND
└─ VCC_RF (U18)
D_ESD (>2 GHz rated, <0.5 pF) at J12
```
Active antenna is **mandatory** — the ZED-F9P has **no internal LNA**.
### S2 — LTE (`J13`)
```
U19.RF_ANT ─── π-match pads (series 0402 + 2× shunt 0402, series = 0 Ω default)
─── J13 (U.FL) ─── D_ESD (low-cap <0.5 pF)
```
### S3 — 🔴 GNSS desense — the #1 technical risk
A 23 dBm transmitter drawing 2–3 A bursts next to a GNSS front end is a classic RTK-killer.
**The Ardusimple shield has been hiding this. A bare module hands it to you.**
1. **Board separation:** `U18` in the freed `MOD1` area (bottom-left), `U19` in the freed
XBee area (right edge) → **≈ 149 mm apart, the full diagonal.** The GNSS corner is also the
furthest point from all four switchers (nearest, `U7` at 140.0/112.9, ~60 mm).
2. **Antenna separation ≥ 20 cm** on the enclosure — two bulkheads. **Design rule, not an
optimization.**
3. Modem return path routed away from the GNSS island; no shared reference but `In1`.
4. **No 1575 SAW** — it would kill L2 on a dual-band receiver. Specify an active antenna with
good out-of-band rejection instead.
5. **Measured at gate G3** (Phase 6): fix quality and C/N₀ with the modem idle vs. transmitting
at full power. Pass/fail, not opinion.
### S4 — ESD everywhere (EMC finding 5)
**Every external connector gets a clamp within 10 mm.** `D14/D15/D16` move from the MCU to
within 10 mm of `J11`. `J9` gets the ESD array it never had (**< 2 pF** on the data lines).
`J12`, `J13`, `P1`, `P4`, `P15`, `P16` all get appropriate clamps.
---
## Block T — `K1` rewiring (one net change, big consequence)
| `K1` pin | v5 | **v6** |
|---|---|---|
| 1 (NC) | `/USART1_RX` | **unchanged** — receiver UART2 TX |
| 5/6 (COM) | `/DIN_4G` | **unchanged** — modem RX |
| 10 (NO) | `/USART1_TX` (PA9) | **`/USART6_TX` (PC6)** ← the only change |
| 2 | `/SYS5V` | unchanged |
| 9 | `/KCOIL` ← `BYP_CTRL` (PB5) | unchanged |
**What this buys.** Energised (normal operation) the MCU owns the modem's RX line: it can send
AT commands, forward the receiver's GGA byte-exact, and inject `$PHLG`. **De-energised — MCU
dead or absent — the relay relaxes and the receiver's own GGA goes straight to the modem, so
corrections keep flowing.** Invariants #2 and #3 both hold, with hardware, for the price of one
moved net.
**Consequence to state plainly: v6 makes Modo B the normal operating mode.** Consistent with
the EPCT decision already on record.
---
## 4. New net list
| Net | Members |
|---|---|
| `/3V3_GNSS` | `U21.out` · `U18.VCC` · decoupling |
| `/VBAT_MODEM` | `L20.2` · `U19.55/56/57` · bulk + ceramics |
| `/VIN_MODEM` | `D20.k` · `D21.k` · `U20.VIN` · Cin |
| `/1V8_MODEM` | `U19.VDD_1V8` · `U23.VccA` · `U24.VccA` · 2.2 µF |
| `/USART6_TX` | `U16.37` (PC6) · `K1.10` |
| `/USART6_RX` | `U16.38` (PC7) · 100 Ω → `/DOUT_4G` (3.3 V side) |
| `/MODEM_PWRKEY` / `_M` | `U16.39` (PC8) → `Q20` → `U19.PWRKEY` |
| `/MODEM_STATUS` / `_M` | `U19.STATUS` → `U24` → `U16.40` (PC9) |
| `/MODEM_PWR_EN` | `U16.51` (PC10) · `U20.EN` |
| `/MODEM_RST` / `_M` | `U16.52` (PC11) → `Q21` → `U19.RESET` |
| `/GNSS_RESET_N` | `U16.53` (PC12) · `U18.RESET_N` · 10 kΩ pull-up |
| `/GNSS_ANT_DETECT` | `U16.9` (PC1) · `U18.ANT_DETECT` |
| `/GNSS_ANT_SHORT_N` | `U16.10` (PC2) · `U18.ANT_SHORT_N` |
| `/GNSS_TIMEPULSE` | `U16.23` (PA7) · `U18.TIMEPULSE` |
| `/SIM_VDD` `/SIM_CLK` `/SIM_DATA` `/SIM_RST` | `U19` ↔ `U22` |
| `/RF_GNSS` `/RF_LTE` | 50 Ω, own netclass, **no autorouting** |
**Changed:** `/DOUT_4G` gains `U18`.UART2_RX, `U24` and the PC7 tap; loses `J7.2`, `MOD1.17`.
`/DIN_4G` gains `U23`/`U19`; loses `J7.3`. `/USART1_RX` loses `MOD1.23`, gains `U18`.UART2_TX.
`/USART1_TX` loses `K1.10` → PA9 becomes a test point.
**Netclasses:** keep `HV24`; add **`RF`** (50 Ω, no auto-route) and **`PWR_MODEM`**
(`/VBAT_MODEM`, `/VIN_MODEM` — min width 1.5 mm, target ≥ 3 mm).
---
## 5. Inherited EMC fixes — schematic-level, day-one
| # | Fix | Where it lands in the schematic |
|---|---|---|
| 🔴 1 | `/SW_BAT` ≥ 2 mm polygon | netclass + layout; **load-bearing** — blackout now feeds the modem too |
| 🔴 2 | `/BAT_PLUS` ≥ 1.5 mm + via farm | same |
| 🟠 3 | `R20`/`R21` at `U2` pin 4, `/FB_FAN` short | schematic grouping so layout can honour it |
| 🟠 4 | 1 µF + 10 µF ≤ 3 mm of every buck VIN — **including `U20`** | add the parts now |
| 🟠 5 | ESD < 10 mm of every external connector | Block S4 |
| 🟡 6 | Crystals → **3225** package, < 5 mm (`Y1`, `Y3`, `Y4` are HC-49 at 12–15 mm) | change footprints in the schematic pass |
| 🟡 7 | ETH RX pair coupled and length-matched | layout, flagged here |
| 🟡 8 | Fill the 8 empty 20 mm stitching cells | free — most were under `MOD1` |
---
## 6. Part additions
| Ref | Part | LCSC | Note |
|---|---|---|---|
| `U18` | ZED-F9P-04B | **C5120257** | Extended, X-ray, ~104–129 in stock — **consign from Mouser for ≥ 100 units** |
| `U19` | A7670SA-LASC | **C18548276** | Extended, X-ray, MSL 3, 220 in stock |
| `U20` | 40 V / 3 A sync buck (AP64350 class) | verify | **40 V minimum** |
| `U21` | TPS7A2033 or AP2112K-3.3 | C51118 (AP2112K) | ⚠ verify the ≤ 66 µs ramp |
| `U22` | MFF2 SIM | carrier | lead-time item |
| `U23`, `U24` | SN74LVC2T45DCUR | verify | direction-fixed |
| `Q20`, `Q21` | 2N7002 | verify | open-drain PWRKEY / RESET |
| `D20` | SS34 | in BOM (`D2`) | 24 V leg |
| `D21` | PMEG3050 class | verify | low-Vf, 5 V leg |
| `J12`, `J13` | U.FL / IPEX MHF1 SMT receptacle | verify | + 2 pigtails, 2 SMA bulkheads |
| `L20` | buck inductor per `U20`'s datasheet | verify | ≥ 4 A saturation |
| — | 2× 220 µF low-ESR + ceramics | verify | Block P bulk |
| — | ESD arrays, RF ESD, bias-tee L/C, π-match 0402s | verify | Blocks S1–S4 |
> **All "verify" rows must be confirmed against the live JLCPCB library at upload**, not from
> a cached DB. The `v4-bom.xls` labels are on record as unreliable; check pin-counts vs
> pad-counts every time.
---
## 7. Execution order & verification
| Step | Work | Check |
|---|---|---|
| T0 | Clear stale `.git/*.lock`; confirm the v5.5 fix commit landed; branch `v6` from `c3e8e48`; copy v5 → `hbox-watchdog-v6` | parse-verify the copy opens clean |
| T1 | Deletions (§2) | netlist parse: `MOD1`/`J7`/`J8`/`SW4G1` gone, no orphan nets |
| T2 | Block T (`K1.10` → PC6) | 1-net diff, nothing else moved |
| T3 | Block N + `U21` | ERC on the GNSS island alone |
| T4 | Block O + Block Q + Block R | ERC; **confirm every 1.8 V line crosses a shifter** |
| T5 | Block P + LTC4040 input-limit fix | recompute the backup power budget vs the 1S4P ~12 Ah pack |
| T6 | Block S (RF + ESD) + netclasses | `RF` netclass exists and is excluded from autorouting |
| T7 | EMC list §5 | each row ticked individually |
| T8 | Full ERC + independent pin-map parse | **ERC 0**; no double-allocated MCU pin |
**Gate G2:** ERC 0 **plus** an external review of the RF and power sections. This is Helgen's
first RF design — do not let ERC-clean stand in for reviewed.
---
## 8. What is deliberately NOT in v6
- **A second GNSS footprint.** LG290P's pads overlap the F9P ring and the functions don't align.
The cost-down is a v7 respin of one corner — which is exactly why Block N is an island.
- **A modem mezzanine daughtercard.** Rejected with "full integration" (brief §0); revisit only
if gate T1's cabinet temperature forces the industrial tier.
- **eUICC.** MFF2 only, per the 24-Ago decision.
- **Cat-4 / WCDMA.** See brief §2.3.
---
## 9. Cross-references
- v6 brief: `2026-08-24_Drogheda_EinarBoard-v6_GNSS-4G-Integration_Design-Brief_v1.0.md`
- Phase 1 firmware: `2026-08-24_Drogheda_EinarBoard-v6_Phase-1_NTRIP-Firmware-Spec_v1.0.md`
- EMC fix list: `2026-08-22_Drogheda_EinarBoard-v5_EMC-Audit_v1.0.md`
- Canonical electrical state: `watchdog-electrical-design-state.md`
- Format precedent: `watchdog-production-schematic-delta.md`
- Cap placement rules: `2026-08-14_Drogheda_EinarBoard-3.0_Capacitor-Placement-Map_v1.0.md`
- Invariants: **#1** power-off safe (`Q20`/`Q21` open-drain, buck EN default off) · **#2**
never vanish (Block P dual input, Block T fail-safe) · **#3** power-cycle recovery
(`MODEM_PWR_EN`) · **#5** no head-of-line blocking · **#6** no secrets

View File

@ -0,0 +1,188 @@
# KiCad → git submodule + Gitea mirror — migration record & runbook
**Date:** 2026-08-27 · **Status:** local side DONE, pushes pending (need network + credentials)
---
## 1. What was done
`03-Architecture-Hardware/kicad` is no longer tracked file-by-file in
`project-drogheda`. It is now a **standalone git repository**, referenced from the
parent as a submodule.
| Item | Value |
|---|---|
| Submodule path | `03-Architecture-Hardware/kicad` |
| Submodule name | `kicad` |
| Canonical remote (`origin`) | `git@github.com:lrodriguez15/hbox-kicad.git` |
| Mirror remote (`helgen`) | `ssh://git@git.helgen.online:222/Helgen-Services/HBox_PCB.git` |
| Submodule branch | `main`, **72 commits**, HEAD `4bf41c6` |
| Parent commit | `5bb2e12` — "kicad → submodule (hbox-kicad)" |
| Rollback tag (parent) | `pre-kicad-submodule-2026-08-26` → `daafd5c` |
**History was preserved.** The 71 board commits were extracted with
`git subtree split --prefix=03-Architecture-Hardware/kicad`, so blame and bisect
still work on the schematic and PCB. Commit **hashes differ** from the parent's
(subtree split rewrites them); messages, authors and dates are identical.
Verified: every file that existed under `kicad/` at `daafd5c` is byte-identical
in the submodule (blob-hash comparison, 309/309 files). `git fsck
--connectivity-only` on the new repo is clean.
### Deliberate content changes (1 commit on top of the split)
- **added** `.gitignore` — KiCad `.kicad_prl`, `*.lck`, `*.T*bak` agent scratch
backups, `.history/`, `.mcp-backups/`, `_to_delete/`, `*_drc_violations.json`,
`*.log`, `snapshots/`, Office lock files
- **added** `README.md` — layout, remotes, history note
- **untracked** `hbox-watchdog-v3.kicad_prl` (per-user KiCad UI state)
- **untracked** `~hbox-watchdog-v5.kicad_pro.lck` (KiCad session lock, already
deleted on disk)
### Not done, on purpose
- **Parent history was not rewritten.** Its `.git` stays ~239 MB because the old
kicad blobs are still in it. Nothing force-pushed, no clone broken. The parent
simply stops growing from here on. If you ever want it slim, that is a separate
`git filter-repo` + force-push job.
- Branch `kicad-split` still exists in the parent as the split's output. Delete it
once the push succeeds: `git branch -D kicad-split`.
---
## 2. What you still have to do
### Step 1 — create the GitHub repo and push the submodule
```bash
cd ~/Documents/Claude/Projects/Drogheda/03-Architecture-Hardware/kicad
# create it (private) — or make it in the web UI as lrodriguez15/hbox-kicad
gh repo create lrodriguez15/hbox-kicad --private
git push -u origin main
```
~240 MB on the first push, mostly the STL/STEP mechanical exports. Expect a few
minutes.
### Step 2 — push the parent
```bash
cd ~/Documents/Claude/Projects/Drogheda
git push origin main
git push origin pre-kicad-submodule-2026-08-26 # rollback tag
git branch -D kicad-split # split scaffolding, no longer needed
```
Push the submodule **first**. A parent commit whose gitlink points at a commit
nobody can fetch will break every clone.
### Step 3 — set up the Gitea pull mirror
Gitea can only make a repo a *pull* mirror **at migration time** — it cannot be
switched on for a repo that already exists. `Helgen-Services/HBox_PCB` is
currently empty, so:
1. Gitea → `Helgen-Services/HBox_PCB` → **Settings → Danger Zone → Delete**.
2. **+ → New Migration → GitHub**.
- Clone URL: `https://github.com/lrodriguez15/hbox-kicad.git`
- Auth: your GitHub username + a **classic PAT with `repo` scope**
(required — the GitHub repo is private)
- Owner: `Helgen-Services` · Repository name: `HBox_PCB`
- ✅ **This repository will be a mirror**
3. After migration: **Settings → Mirror Settings** → set the interval (default is
`8h`; `1h` is reasonable) and use **Synchronize Now** to test.
You push only to GitHub; Gitea follows on its own. **This needs no SSH key** —
the mirror pulls over HTTPS with the PAT, which is why it is the low-friction
option while your SSH credentials are still pending.
If you would rather keep the existing `HBox_PCB` repo as-is, the fallback is a
local dual push instead of a server-side mirror:
```bash
cd .../kicad
git remote set-url --add --push origin git@github.com:lrodriguez15/hbox-kicad.git
git remote set-url --add --push origin ssh://git@git.helgen.online:222/Helgen-Services/HBox_PCB.git
# now `git push origin main` hits both — but both must be reachable, every time
```
### Step 4 — SSH to Gitea (only if you want to push there directly)
```bash
ssh-keygen -t ed25519 -C "leo@helgen.tech" -f ~/.ssh/id_ed25519_helgen
pbcopy < ~/.ssh/id_ed25519_helgen.pub # paste into Gitea → Settings → SSH/GPG Keys
cat >> ~/.ssh/config <<'EOF'
Host git.helgen.online
HostName git.helgen.online
Port 222
User git
IdentityFile ~/.ssh/id_ed25519_helgen
EOF
ssh -T git@git.helgen.online -p 222 # expect a Gitea greeting
```
The `helgen` remote is already configured in the submodule, so after this
`git push helgen main` works. **Do not** push to Gitea if you set it up as a pull
mirror in Step 3 — a mirror is read-only and force-overwrites local pushes on the
next sync.
---
## 3. Day-to-day workflow from now on
Board work is committed **inside** the submodule, then the parent is told which
commit to point at:
```bash
cd .../Drogheda/03-Architecture-Hardware/kicad
git add hbox-watchdog-v6.kicad_sch hbox-watchdog-v6.kicad_pcb
git commit -m "v6 T5: LTC4040 errata + U7 delete"
git push # → GitHub, Gitea follows
cd ../.. # parent repo
git add 03-Architecture-Hardware/kicad # stages the new gitlink
git commit -m "kicad: v6 T5"
git push
```
Forgetting the second half is the classic submodule trap: the board work is safe
on GitHub, but the parent still points at the old commit.
### Cloning fresh
```bash
git clone --recurse-submodules git@github.com:lrodriguez15/project-drogheda.git
# already cloned without it:
git submodule update --init --recursive
```
Anyone who only has Gitea access can redirect the submodule locally without
editing `.gitmodules`:
```bash
git config --global url."ssh://git@git.helgen.online:222/Helgen-Services/HBox_PCB.git".insteadOf \
"git@github.com:lrodriguez15/hbox-kicad.git"
```
For a fast, board-files-only checkout: `git clone --filter=blob:none` skips the
historical STL/STEP blobs until something actually needs them.
---
## 4. Open items / observations
- **`~hbox-watchdog-v6.kicad_pro` was modified while this ran** — KiCad is (or
was) open on the v6 project. `hbox-watchdog-v6.kicad_pro` shows as modified in
the submodule; commit or discard it as you see fit.
- **Your other three submodules point at `ssh://git.helgen.dev:6611/…`**
(`hbox-firmware`, `htracker`, `hbox-log-tools`) and are currently uninitialised.
If `.dev:6611` has been superseded by `git.helgen.online:222`, those URLs need
updating too — say the word and I'll do it in one pass.
- **This VM has no network access**, which is why every push above is left for
you to run. Everything that could be done offline is done.
- `_to_delete/` folders under `03-Architecture-Hardware/` and `kicad/` hold git
lock/temp files that the sandbox was not permitted to unlink. Safe to delete.

View File

@ -0,0 +1,175 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>HBOX v6 — Antenna Chain</title>
<style>
:root { color-scheme: light dark; }
body { margin: 0; padding: 20px; font: 14px/1.5 system-ui, sans-serif;
background: light-dark(#fafaf8, #16181d); color: light-dark(#1c1e21, #e4e6e9); max-width: 1020px; margin-inline: auto; }
h1 { font-size: 19px; margin: 0 0 4px; }
p.lead { margin: 0 0 22px; color: light-dark(#555, #9aa0a8); }
figure { margin: 0 0 30px; }
svg { max-width: 100%; height: auto; display: block; }
figcaption { font-size: 12.5px; color: light-dark(#555, #9aa0a8); margin-top: 8px; max-width: 62em; }
.rf { color: #3b82f6; } .dc { color: #e08a1f; }
b.k { font-weight: 600; }
</style>
</head>
<body>
<h1>How the GNSS antenna connection works</h1>
<p class="lead">One coax does two jobs at once: <span class="rf"><b>RF comes down</b></span> from the antenna to the receiver, and <span class="dc"><b>3.3&nbsp;V goes up</b></span> the same cable to power the amplifier inside the antenna.</p>
<figure>
<svg viewBox="0 0 980 400" role="img" aria-label="GNSS signal chain: active antenna outside the enclosure, SMA bulkhead through the wall, pigtail to the board U.FL, then bias tee and DC block feeding the ZED-F9P. RF flows right to left into the receiver; 3.3 V flows left to right up the same coax into the antenna's LNA.">
<defs>
<marker id="aRF" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#3b82f6"/>
</marker>
<marker id="aDC" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#e08a1f"/>
</marker>
</defs>
<!-- ======== PCB zone ======== -->
<rect x="14" y="60" width="392" height="240" rx="8" fill="none" stroke="currentColor" stroke-width="1.4"/>
<text x="26" y="84" font-size="13" font-weight="600" fill="currentColor">EinarBoard v6 (PCB)</text>
<!-- F9P -->
<rect x="30" y="130" width="104" height="70" rx="4" fill="none" stroke="currentColor" stroke-width="1.6"/>
<text x="82" y="158" font-size="12.5" font-weight="600" text-anchor="middle" fill="currentColor">ZED-F9P</text>
<text x="82" y="174" font-size="11" text-anchor="middle" fill="currentColor">RF_IN</text>
<text x="82" y="190" font-size="11" text-anchor="middle" fill="currentColor">VCC_RF 3.3 V</text>
<!-- DC block C143 -->
<line x1="134" y1="150" x2="176" y2="150" stroke="#3b82f6" stroke-width="2" marker-start="url(#aRF)"/>
<line x1="182" y1="138" x2="182" y2="162" stroke="currentColor" stroke-width="2"/>
<line x1="190" y1="138" x2="190" y2="162" stroke="currentColor" stroke-width="2"/>
<text x="186" y="128" font-size="11" text-anchor="middle" fill="currentColor">C143 100 nF</text>
<text x="186" y="115" font-size="10.5" text-anchor="middle" fill="currentColor">DC block: RF passes, DC stops</text>
<!-- node to U.FL -->
<line x1="190" y1="150" x2="330" y2="150" stroke="#3b82f6" stroke-width="2"/>
<circle cx="252" cy="150" r="3.2" fill="currentColor"/>
<!-- bias tee L21 -->
<path d="M252 150 v22 m0 0 a6 6 0 0 1 0 12 a6 6 0 0 1 0 12 a6 6 0 0 1 0 12 v14" fill="none" stroke="#e08a1f" stroke-width="2"/>
<text x="264" y="196" font-size="11" fill="currentColor">L21 68 nH</text>
<text x="264" y="210" font-size="10.5" fill="currentColor">bias tee: DC passes,</text>
<text x="264" y="223" font-size="10.5" fill="currentColor">RF blocked</text>
<!-- DC from VCC_RF -->
<path d="M134 186 h84 v36 h34" fill="none" stroke="#e08a1f" stroke-width="2"/>
<line x1="252" y1="222" x2="252" y2="156" stroke="#e08a1f" stroke-width="0" />
<text x="150" y="240" font-size="11" fill="#e08a1f">3.3 V, ≤50 mA</text>
<!-- ESD -->
<line x1="306" y1="150" x2="306" y2="184" stroke="currentColor" stroke-width="1.4"/>
<path d="M298 184 h16 l-8 14 z" fill="none" stroke="currentColor" stroke-width="1.4"/>
<line x1="298" y1="198" x2="314" y2="198" stroke="currentColor" stroke-width="1.6"/>
<line x1="300" y1="203" x2="312" y2="203" stroke="currentColor" stroke-width="1.2"/>
<line x1="303" y1="208" x2="309" y2="208" stroke="currentColor" stroke-width="1"/>
<text x="320" y="192" font-size="11" fill="currentColor">D27 ESD 0.5 pF</text>
<!-- U.FL -->
<circle cx="352" cy="150" r="16" fill="none" stroke="currentColor" stroke-width="1.6"/>
<circle cx="352" cy="150" r="4" fill="currentColor"/>
<text x="352" y="106" font-size="11.5" font-weight="600" text-anchor="middle" fill="currentColor">J12</text>
<text x="352" y="120" font-size="11" text-anchor="middle" fill="currentColor">U.FL</text>
<!-- ======== pigtail ======== -->
<path d="M368 150 C 420 150 430 150 470 150" fill="none" stroke="currentColor" stroke-width="5" opacity="0.28"/>
<line x1="368" y1="150" x2="470" y2="150" stroke="#3b82f6" stroke-width="2"/>
<text x="419" y="132" font-size="11" text-anchor="middle" fill="currentColor">pigtail 10–15 cm</text>
<!-- ======== enclosure wall ======== -->
<rect x="470" y="40" width="18" height="330" fill="currentColor" opacity="0.16"/>
<text x="479" y="30" font-size="11.5" font-weight="600" text-anchor="middle" fill="currentColor">enclosure wall</text>
<!-- bulkhead SMA -->
<rect x="458" y="138" width="44" height="24" rx="3" fill="none" stroke="currentColor" stroke-width="1.8"/>
<line x1="452" y1="134" x2="452" y2="166" stroke="currentColor" stroke-width="3"/>
<line x1="508" y1="134" x2="508" y2="166" stroke="currentColor" stroke-width="3"/>
<text x="480" y="184" font-size="11.5" font-weight="600" text-anchor="middle" fill="currentColor">SMA bulkhead</text>
<text x="480" y="198" font-size="10.5" text-anchor="middle" fill="currentColor">nut + gasket seal (IP)</text>
<text x="480" y="212" font-size="10.5" text-anchor="middle" fill="currentColor">takes the torque, not the PCB</text>
<!-- ======== outside ======== -->
<line x1="508" y1="150" x2="640" y2="150" stroke="#3b82f6" stroke-width="2"/>
<path d="M508 150 C 560 150 580 150 640 150" fill="none" stroke="currentColor" stroke-width="5" opacity="0.28"/>
<text x="574" y="132" font-size="11" text-anchor="middle" fill="currentColor">antenna cable (SMA macho)</text>
<!-- active antenna -->
<rect x="640" y="92" width="240" height="120" rx="8" fill="none" stroke="currentColor" stroke-width="1.6"/>
<text x="760" y="82" font-size="12.5" font-weight="600" text-anchor="middle" fill="currentColor">Active GNSS antenna (outside, clear sky)</text>
<rect x="660" y="112" width="80" height="34" rx="3" fill="none" stroke="currentColor" stroke-width="1.4"/>
<text x="700" y="133" font-size="11" text-anchor="middle" fill="currentColor">patch L1/L2</text>
<rect x="780" y="112" width="66" height="34" rx="3" fill="none" stroke="currentColor" stroke-width="1.4"/>
<text x="813" y="133" font-size="11" text-anchor="middle" fill="currentColor">LNA</text>
<line x1="740" y1="129" x2="780" y2="129" stroke="#3b82f6" stroke-width="2" marker-end="url(#aRF)"/>
<line x1="846" y1="129" x2="874" y2="129" stroke="#3b82f6" stroke-width="2"/>
<path d="M874 129 v21 h-234 v0" fill="none" stroke="#3b82f6" stroke-width="2"/>
<text x="762" y="172" font-size="10.5" text-anchor="middle" fill="currentColor">the LNA runs on the 3.3 V that arrives up the coax</text>
<text x="762" y="196" font-size="10.5" text-anchor="middle" fill="currentColor">ground plane / metal surface under the antenna kills reflections</text>
<!-- satellites -->
<path d="M690 34 l-14 22 M710 30 l-6 26 M732 34 l4 22" stroke="#3b82f6" stroke-width="1.6" fill="none" marker-end="url(#aRF)"/>
<text x="712" y="24" font-size="11" text-anchor="middle" fill="currentColor">L1 / L2 from satellites</text>
<!-- flow legend arrows -->
<line x1="640" y1="290" x2="140" y2="290" stroke="#3b82f6" stroke-width="2.4" marker-end="url(#aRF)"/>
<text x="390" y="282" font-size="12" text-anchor="middle" fill="#3b82f6">RF signal → down the coax → into RF_IN</text>
<line x1="140" y1="330" x2="640" y2="330" stroke="#e08a1f" stroke-width="2.4" marker-end="url(#aDC)"/>
<text x="390" y="352" font-size="12" text-anchor="middle" fill="#e08a1f">3.3 V DC → up the same coax → powers the antenna's LNA</text>
</svg>
<figcaption><b class="k">Fig. 1 — the same piece of coax carries both directions.</b> On the board, L21 (the bias tee) injects 3.3&nbsp;V onto the antenna side of the line — an inductor passes DC but looks like a wall to 1.5&nbsp;GHz RF, so the injection doesn't disturb the signal. C143 (the DC block) does the mirror job: RF passes into the receiver, DC cannot. Result: the antenna's built-in amplifier gets its power with zero extra wires, which is why the F9P <i>requires</i> an active antenna and why J12's little circuit exists.</figcaption>
</figure>
<figure>
<svg viewBox="0 0 980 300" role="img" aria-label="Top view of the cabinet: the PCB's GNSS U.FL at the bottom-left corner and LTE U.FL at the right edge run short pigtails to two SMA bulkheads on the enclosure wall, mounted at least 20 cm apart. The GNSS antenna mounts high with clear sky; the LTE whip mounts on the opposite side.">
<!-- cabinet -->
<rect x="150" y="50" width="560" height="220" rx="10" fill="none" stroke="currentColor" stroke-width="1.8"/>
<text x="430" y="40" font-size="12.5" font-weight="600" text-anchor="middle" fill="currentColor">HBOX cabinet (top view)</text>
<!-- PCB -->
<rect x="230" y="90" width="400" height="140" rx="4" fill="none" stroke="currentColor" stroke-width="1.3" stroke-dasharray="5 4"/>
<text x="430" y="110" font-size="11.5" text-anchor="middle" fill="currentColor">EinarBoard v6 — 140 × 100</text>
<circle cx="252" cy="208" r="7" fill="none" stroke="currentColor" stroke-width="1.5"/>
<text x="252" y="232" font-size="10.5" text-anchor="middle" fill="currentColor">J12 GNSS</text>
<circle cx="608" cy="130" r="7" fill="none" stroke="currentColor" stroke-width="1.5"/>
<text x="608" y="112" font-size="10.5" text-anchor="middle" fill="currentColor">J13 LTE</text>
<line x1="269" y1="196" x2="591" y2="142" stroke="currentColor" stroke-width="1" stroke-dasharray="3 4"/>
<text x="430" y="182" font-size="10.5" text-anchor="middle" fill="currentColor">≈149 mm apart on the board</text>
<!-- pigtails to wall -->
<path d="M245 208 C 200 208 190 200 160 190" fill="none" stroke="currentColor" stroke-width="2.2" opacity="0.55"/>
<path d="M615 130 C 660 130 680 122 700 116" fill="none" stroke="currentColor" stroke-width="2.2" opacity="0.55"/>
<!-- bulkheads -->
<rect x="142" y="180" width="16" height="20" fill="none" stroke="currentColor" stroke-width="1.8"/>
<rect x="702" y="106" width="16" height="20" fill="none" stroke="currentColor" stroke-width="1.8"/>
<text x="128" y="216" font-size="10.5" text-anchor="end" fill="currentColor">SMA bulkhead</text>
<text x="732" y="100" font-size="10.5" fill="currentColor">SMA bulkhead</text>
<!-- separation arrow -->
<line x1="150" y1="286" x2="710" y2="286" stroke="currentColor" stroke-width="1.4" marker-start="url(#aSep)" marker-end="url(#aSep)"/>
<defs>
<marker id="aSep" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="currentColor"/>
</marker>
</defs>
<text x="430" y="280" font-size="11.5" font-weight="600" text-anchor="middle" fill="currentColor">antennas ≥ 20 cm apart — the #1 defence against the modem deafening the GNSS</text>
<!-- external antennas -->
<line x1="100" y1="190" x2="142" y2="190" stroke="#3b82f6" stroke-width="2"/>
<circle cx="84" cy="190" r="15" fill="none" stroke="#3b82f6" stroke-width="1.8"/>
<circle cx="84" cy="190" r="4" fill="#3b82f6"/>
<text x="84" y="164" font-size="10.5" text-anchor="middle" fill="currentColor">GNSS antenna</text>
<text x="84" y="222" font-size="10.5" text-anchor="middle" fill="currentColor">highest point,</text>
<text x="84" y="234" font-size="10.5" text-anchor="middle" fill="currentColor">clear sky</text>
<line x1="718" y1="116" x2="760" y2="116" stroke="currentColor" stroke-width="2"/>
<line x1="760" y1="116" x2="760" y2="66" stroke="currentColor" stroke-width="2.6"/>
<text x="792" y="86" font-size="10.5" fill="currentColor">LTE whip</text>
</svg>
<figcaption><b class="k">Fig. 2 — where each piece lives.</b> The board carries only the two U.FL receptacles, placed at opposite corners. Short pigtails jump to SMA bulkheads that clamp through the enclosure wall — the nut and gasket keep the seal, and any torque from screwing antennas on lands in the wall, never in the PCB's solder joints. Outside, the antennas separate further: GNSS up high with open sky, the LTE whip away from it.</figcaption>
</figure>
</body>
</html>

@ -1 +1 @@
Subproject commit 23ed361f0d1bbb72e6ba2978d9f0834dae5c9e79
Subproject commit cc1c4c12a4055f703b8862fb5b14ea2876601dff

View File

@ -0,0 +1,234 @@
#!/usr/bin/env python3
"""v6 T6 - Block S: RF front ends (J12 GNSS + bias tee, J13 LTE + pi-pad), RF ESD,
P17 u-center USB port, and the RF / PWR_MODEM netclasses.
Parts: U.FL = C88374 (fp RF-SMD_FRF05002-JSS103M, 1=SIG 2/3=GND shield);
RF ESD = ESD9L5.0ST5G C82326 (0.5 pF - the PESD used elsewhere is ~70 pF, NOT RF-safe).
Usage: python3 2026-08-25_v6_T6_rf_block.py [--apply]
"""
import os, sys, shutil, uuid, re, json, glob
KI = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '03-Architecture-Hardware', 'kicad')
SCH = os.path.join(KI, 'hbox-watchdog-v6.kicad_sch')
VSYM= os.path.join(KI, 'Drogheda-v6.kicad_sym')
PRO = os.path.join(KI, 'hbox-watchdog-v6.kicad_pro')
APPLY = '--apply' in sys.argv
ROOT = '/561403b3-895e-4153-a7be-3876ae930d43'
def f(v):
v = 0.0 if abs(v) < 1e-9 else v
return '%.10g' % v
def bal(t):
d=0;i=False;e=False
for ch in t:
if e: e=False; continue
if ch=='\\': e=True; continue
if ch=='"': i=not i; continue
if i: continue
if ch=='(': d+=1
elif ch==')': d-=1
return d
def span_from(text,i):
d=0;instr=False;esc=False;j=i
while j<len(text):
ch=text[j]
if esc: esc=False
elif ch=='\\': esc=True
elif ch=='"': instr=not instr
elif not instr:
if ch=='(': d+=1
elif ch==')':
d-=1
if d==0: return j+1
j+=1
raise ValueError('unbalanced')
PIN_RE = re.compile(r'\(pin \w+ \w+\s*\(?at ([-\d.]+) ([-\d.]+) (\d+)\)(?:(?!\(pin ).)*?\(name "([^"]*)"(?:(?!\(pin ).)*?\(number "([^"]+)"', re.S)
PIN_RE2= re.compile(r'\(pin \w+ \w+\s*\n\s*\(at ([-\d.]+) ([-\d.]+) (\d+)\)(?:(?!\(pin ).)*?\(name "([^"]*)"(?:(?!\(pin ).)*?\(number "([^"]+)"', re.S)
def libpins(libsrc,name):
i=libsrc.find('(symbol "%s"'%name)
if i<0: return None
j=libsrc.find('\n (symbol "',i+10)
if j<0: j=libsrc.find('\n\t\t(symbol "',i+10)
body=libsrc[i: j if j>0 else len(libsrc)]
pins=PIN_RE2.findall(body) or PIN_RE.findall(body)
return [(num,nm,float(x),float(y)) for x,y,rot,nm,num in pins]
sch=open(SCH,encoding='utf-8').read()
vsym=open(VSYM,encoding='utf-8').read()
# --- prerequisites ---
esd_syms=[n for n in re.findall(r'\n \(symbol "([^"]+)"',vsym) if 'ESD9L' in n]
if not esd_syms: sys.exit('ESD9L5.0ST5G not in Drogheda-v6.kicad_sym - run easyeda2kicad C82326 first')
ESDN=esd_syms[0]
esd_fp_files=[os.path.basename(p)[:-10] for p in glob.glob(os.path.join(KI,'Drogheda-v6.pretty','*.kicad_mod'))
if 'SOD' in os.path.basename(p).upper() and '128' not in os.path.basename(p)]
if not esd_fp_files: sys.exit('no SOD-9xx footprint found for the ESD9L part')
ESDFP='Drogheda-v6:'+esd_fp_files[0]
esdp=libpins(vsym,ESDN)
print('ESD symbol %s pins: %s fp=%s'%(ESDN,[(n,nm) for n,nm,_,_ in esdp],ESDFP))
# cathode pad = the one NOT named A/anode; EasyEDA usually 1=A? assert explicit naming
kpin=[n for n,nm,_,_ in esdp if nm.upper() in ('K','C','CATHODE','2','1') and nm.upper() in ('K','C','CATHODE')]
apin=[n for n,nm,_,_ in esdp if nm.upper() in ('A','ANODE')]
if not (kpin and apin):
# fall back: unidirectional SOD-923 convention on EasyEDA symbols: pin named by number
sys.exit('ESD9L pin names ambiguous: %s - inspect and hardcode before applying'%[(n,nm) for n,nm,_,_ in esdp])
KP,AP=kpin[0],apin[0]
UFL=libpins(vsym,'U.FL-R-SMT-1')
if not UFL: sys.exit('U.FL-R-SMT-1 symbol missing')
lib_cached=sch.split('(lib_symbols',1)[1]
def cachedpins(name):
pp=libpins(lib_cached,name)
return pp
# --- cache new symbols in sch ---
def grab(libsrc,name,prefix):
i=libsrc.find(' (symbol "%s"\n'%name)
if i<0: sys.exit('symbol %s not found'%name)
j=libsrc.index('(',i); k=span_from(libsrc,j)
return libsrc[j:k].replace('(symbol "%s"'%name,'(symbol "%s:%s"'%(prefix,name),1)
cache=''
for nm in ('U.FL-R-SMT-1',ESDN):
if '(symbol "Drogheda-v6:%s"'%nm not in sch:
cache+='\n\t\t'+grab(vsym,nm,'Drogheda-v6')
m=re.search(r'\n\t\(lib_symbols\n',sch)
k=span_from(sch,sch.index('(',m.start()))
sch=sch[:k-1]+cache+'\n\t'+sch[k-1:]
# --- helpers ---
add=''; nlab=0; newlabels=[]
def add_lab(net,x,y,rot,just):
global add,nlab
add+=('\n\t(label "%s"\n\t\t(at %s %s %d)\n\t\t(effects\n\t\t\t(font\n\t\t\t\t(size 1.27 1.27)\n'
'\t\t\t)\n\t\t\t(justify %s)\n\t\t)\n\t\t(uuid "%s")\n\t)')%(net,f(x),f(y),rot,just,uuid.uuid4())
nlab+=1; newlabels.append((net,round(x,3),round(y,3)))
def orient(px,py):
if abs(px)<0.01: return 90,'left bottom'
return (0,'left bottom') if px>0 else (180,'right bottom')
def mksym(lib_id,ref,val,x,y,fp,npins,lcsc='',dnp=False):
props=[('Reference',ref,x-3,y-3,False),('Value',val,x+3,y-3,False),('Footprint',fp,x,y,True),
('Datasheet','~',x,y,True),('Description','',x,y,True)]
if lcsc: props.append(('LCSC',lcsc,x,y,True))
p=''
for nm,v,px_,py_,hide in props:
h='\n\t\t\t(hide yes)' if hide else ''
p+=('\t\t(property "%s" "%s"\n\t\t\t(at %s %s 0)%s\n\t\t\t(show_name no)\n\t\t\t(do_not_autoplace no)\n'
'\t\t\t(effects\n\t\t\t\t(font\n\t\t\t\t\t(size 1.27 1.27)\n\t\t\t\t)\n\t\t\t)\n\t\t)\n')%(nm,v,f(px_),f(py_),h)
pn=''.join('\t\t(pin "%s"\n\t\t\t(uuid "%s")\n\t\t)\n'%(n,uuid.uuid4()) for n in npins)
return ('\n\t(symbol\n\t\t(lib_id "%s")\n\t\t(at %s %s 0)\n\t\t(unit 1)\n\t\t(body_style 1)\n'
'\t\t(exclude_from_sim no)\n\t\t(in_bom yes)\n\t\t(on_board yes)\n\t\t(in_pos_files yes)\n'
'\t\t(dnp %s)\n\t\t(uuid "%s")\n%s%s\t\t(instances\n\t\t\t(project "hbox-watchdog-v6"\n'
'\t\t\t\t(path "%s"\n\t\t\t\t\t(reference "%s")\n\t\t\t\t\t(unit 1)\n\t\t\t\t)\n\t\t\t)\n\t\t)\n\t)'
)%(lib_id,f(x),f(y),'yes' if dnp else 'no',uuid.uuid4(),p,pn,ROOT,ref)
def place(lib_id,ref,val,x,y,fp,netmap,lcsc='',dnp=False,src=None):
global add
pp=libpins(src,lib_id.split(':',1)[1]) if src is not None else cachedpins(lib_id)
if not pp: sys.exit('no pins for %s'%lib_id)
add+=mksym(lib_id,ref,val,x,y,fp,[n for n,_,_,_ in pp],lcsc,dnp)
for num,_,px,py in pp:
rot,just=orient(px,py)
add_lab(netmap[num],x+px,y-py,rot,just)
UFLFP='Drogheda-v6:RF-SMD_FRF05002-JSS103M'
# ----- S1: GNSS chain -----
place('Drogheda-v6:U.FL-R-SMT-1','J12','UFL-GNSS',400.0,155.0,UFLFP,{'1':'RF_ANT_GNSS','2':'GND','3':'GND'},'C88374',src=vsym)
place('Device:C','C143','100nF-RF',418.0,150.0,'Capacitor_SMD:C_0402_1005Metric',{'1':'RF_ANT_GNSS','2':'RF_GNSS'})
place('Device:L','L21','68nH',424.0,160.0,'Inductor_SMD:L_0603_1608Metric',{'1':'RF_ANT_GNSS','2':'VCC_RF_GNSS'})
place('Device:C','C144','100nF',432.0,160.0,'Capacitor_SMD:C_0402_1005Metric',{'1':'VCC_RF_GNSS','2':'GND'})
place('Device:C','C145','100pF',438.0,160.0,'Capacitor_SMD:C_0402_1005Metric',{'1':'VCC_RF_GNSS','2':'GND'})
place('Drogheda-v6:'+ESDN,'D27','ESD9L5.0ST5G',406.0,165.0,ESDFP,{KP:'RF_ANT_GNSS',AP:'GND'},'C82326',src=vsym)
# ----- S2: LTE chain -----
place('Device:C','C146','DNP-pi',296.0,262.0,'Capacitor_SMD:C_0402_1005Metric',{'1':'RF_LTE','2':'GND'},dnp=True)
place('Device:R','R118','0R',302.0,262.0,'Resistor_SMD:R_0402_1005Metric',{'1':'RF_LTE','2':'RF_ANT_LTE'})
place('Device:C','C147','DNP-pi',308.0,262.0,'Capacitor_SMD:C_0402_1005Metric',{'1':'RF_ANT_LTE','2':'GND'},dnp=True)
place('Drogheda-v6:U.FL-R-SMT-1','J13','UFL-LTE',330.0,262.0,UFLFP,{'1':'RF_ANT_LTE','2':'GND','3':'GND'},'C88374',src=vsym)
place('Drogheda-v6:'+ESDN,'D28','ESD9L5.0ST5G',344.0,262.0,ESDFP,{KP:'RF_ANT_LTE',AP:'GND'},'C82326',src=vsym)
# ----- P17: u-center USB port -----
place('Connector_Generic:Conn_01x04','P17','GNSS-USB-UCENTER',410.0,185.0,
'Connector_JST:JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical',
{'1':'GNSS_VBUS','2':'GNSS_USB_DM','3':'GNSS_USB_DP','4':'GND'})
place('Device:C','C148','1uF',428.0,185.0,'Capacitor_SMD:C_0603_1608Metric',{'1':'GNSS_VBUS','2':'GND'})
d14lib=None
for b in re.finditer(r'\n\t\(symbol\n(?:(?!\n\t\(symbol\n).)*',sch,re.S):
blk=b.group(0)
if re.search(r'\(property "Reference" "D14"',blk):
d14lib=re.search(r'\(lib_id "([^"]+)"',blk).group(1); break
for ref,sig,x in [('D29','GNSS_USB_DM',436.0),('D30','GNSS_USB_DP',442.0),('D31','GNSS_VBUS',448.0)]:
place(d14lib,ref,'PESD5V0S1BA',x,185.0,'Diode_SMD:D_SOD-323',{'1':sig,'2':'GND'},'C2827694')
# labels on U18 USB pins (replacing the no_connects removed below)
add_lab('GNSS_VBUS',436.98,107.3,180,'right bottom')
add_lab('GNSS_USB_DM',503.02,116.19,0,'left bottom')
add_lab('GNSS_USB_DP',503.02,118.73,0,'left bottom')
# --- remove the 3 no_connects on U18 V_USB / USB_DM / USB_DP ---
STALE={(436.98,107.3),(503.02,116.19),(503.02,118.73)}
removed=0; out=[]; last=0
for m in re.finditer(r'\n\t\(no_connect\s*\n\s*\(at ([-\d.]+) ([-\d.]+)\)(?:(?!\n\t\().)*?\n\t\)',sch,re.S):
c=(round(float(m.group(1)),3),round(float(m.group(2)),3))
if c in STALE:
out.append(sch[last:m.start()]); last=m.end(); removed+=1
out.append(sch[last:]); sch=''.join(out)
# --- collision guard ---
labs=set()
for m in re.finditer(r'\((?:label|global_label) "[^"]+"\s*\n\s*\(at ([-\d.]+) ([-\d.]+)',sch):
labs.add((round(float(m.group(1)),3),round(float(m.group(2)),3)))
wire_vts=set()
for m in re.finditer(r'\(wire\s*\n\s*\(pts\s*\n((?:\s*\(xy [-\d.]+ [-\d.]+\)\s*\n?)+)',sch):
for a,b in re.findall(r'\(xy ([-\d.]+) ([-\d.]+)\)',m.group(1)):
wire_vts.add((round(float(a),3),round(float(b),3)))
pins=set()
for b in re.finditer(r'\n\t\(symbol\n(?:(?!\n\t\(symbol\n).)*',sch,re.S):
blk=b.group(0)
l=re.search(r'\(lib_id "([^"]+)"',blk); a=re.search(r'\(at ([-\d.]+) ([-\d.]+) (\d+)\)',blk)
if not(l and a): continue
for pp in (cachedpins(l.group(1)) or []):
pins.add((round(float(a.group(1))+pp[2],3),round(float(a.group(2))-pp[3],3)))
INTENTIONAL={(436.98,107.3),(503.02,116.19),(503.02,118.73)}
existing=(labs|wire_vts|pins)-INTENTIONAL
coll=[(n,x,y) for n,x,y in newlabels if (x,y) in existing]
if coll: sys.exit('COLLISION: %s'%coll[:6])
end=sch.rstrip(); assert end.endswith(')')
sch=end[:-1].rstrip('\n')+add+'\n)\n'
# --- netclasses in the .kicad_pro (JSON) ---
pro=json.load(open(PRO,encoding='utf-8'))
ns=pro.setdefault('net_settings',{})
classes=ns.setdefault('classes',[])
names=[c.get('name') for c in classes]
proto=None
for c in classes:
if c.get('name') not in ('Default',None): proto=c; break
if proto is None and classes: proto=classes[0]
def mkclass(name,width,clearance):
base=dict(proto) if proto else {}
base.update({'name':name,'track_width':width,'clearance':clearance})
base.pop('nets',None)
return base
added_classes=[]
if 'RF' not in names:
classes.append(mkclass('RF',0.35,0.25)); added_classes.append('RF')
if 'PWR_MODEM' not in names:
classes.append(mkclass('PWR_MODEM',1.5,0.2)); added_classes.append('PWR_MODEM')
ncp=ns.setdefault('netclass_patterns',[])
pats={p.get('pattern') for p in ncp}
for net,cls in [('/RF_GNSS','RF'),('/RF_ANT_GNSS','RF'),('/RF_LTE','RF'),('/RF_ANT_LTE','RF'),
('/VBAT_MODEM','PWR_MODEM'),('/VIN_MODEM','PWR_MODEM'),('/SW_MODEM','PWR_MODEM')]:
if net not in pats:
ncp.append({'netclass':cls,'pattern':net})
print('added: J12+bias tee (C143 DC block, L21 68nH, C144/C145), D27 | J13+pi (C146/C147 DNP, R118 0R), D28')
print('P17 u-center port + C148 + D29/D30/D31 PESD | U18 USB labels on, %d no_connects removed'%removed)
print('netclasses added: %s + 7 net patterns'%added_classes)
print('labels added: %d | paren: %d'%(nlab,bal(sch)))
if not APPLY:
print('\nDRY RUN - nothing written. Re-run with --apply.'); sys.exit(0)
if bal(sch)!=0: sys.exit('REFUSING: paren imbalance')
shutil.copy2(SCH,SCH+'.T6bak'); shutil.copy2(PRO,PRO+'.T6bak')
open(SCH,'w',encoding='utf-8').write(sch)
json.dump(pro,open(PRO,'w',encoding='utf-8'),indent=2)
print('\nAPPLIED. Backups: *.T6bak')