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

19 KiB
Raw Blame History

title tags project author date status authority
EinarBoard v6 — Phase 1 — NTRIP Client in the STM32 — Bench Spec & Execution Prompt
project-drogheda
firmware
v6
ntrip
a7670sa
4g
rtcm
bench
design-brief
prompt
Project Drogheda Helgen CTO + Claude 2026-08-24 Active — THE spec for the Phase 1 bench spike. Execute before any v6 layout exists. 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 (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 F411CE (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:

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