Drogheda/03-Architecture-Hardware/2026-08-24_Drogheda_EinarBoard-v6_Phase-1_NTRIP-Firmware-Spec_v1.0.md

338 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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)