--- 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) !!! warning "Designators from before 22-Oct (T43)" This document keeps the designators the board had on its date. Since 22-Oct (T43) the V5.6, V5.7 and v6 carry new ones, and several old names now belong to another part: on the v6, U6 is now the A7670SA modem (was U19) and the LTC4040 is U17 (was U6). To read it against today's board, use the [old → new table](https://drogheda.helgen.online/hardware/designadores-t43-22oct/#8-tablas-viejo-nuevo) (in Spanish); parts already gone by 22-Oct are not in it. > **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",""` · `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","",` | 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 / HTTP/1.0\r\n User-Agent: NTRIP HelgenHBOX/3.1\r\n Authorization: Basic \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)