We build Septentrio-powered GNSS receivers at UAV GNSS — so the message "I wired it up, the port is open, and the autopilot still says no fix" is the one we get most often. In our experience it is almost never the receiver. It is a port, baud, or protocol-direction mismatch, and all three look identical from the outside.
Here is the checklist we walk integrators through, plus a small script that tells you which of those failure modes you are actually in instead of guessing.
1. Count the serial ports before you plan the build
The port count is a property of the receiver, not of the connector you plug in. Counted from the official interface maps:
| Receiver | GNSS engine | Serial ports | Other links |
|---|---|---|---|
| HB10 | AsteRx-m3 Pro+ | 3x UART (LVTTL, 115200 bps - 4 Mbps) | USB-C, microSD, 1PPS out, EVENT in |
| HB59 | AsteRx-m3 Pro+ | 3x UART (LVTTL) | 100M Ethernet, USB-C, microSD, 1PPS, EVENT |
| HB6 / HB6 Pro | mosaic-X5 | 2x UART | USB (4G on Pro) |
| EV322 / HB51 / HB52 | mosaic-G5 | 2x UART (TTL) + I2C | PPS, EVENT |
| HB3 (machine-control box) | AsteRx-m3 Pro+ | SER M12 | CAN (PWR) M12 x2, Ethernet M12, 4G LTE, UHF, WiFi/BT |
Two things worth internalising immediately:
These UARTs are LVTTL (3.3 V logic) at the module. They are not RS-232, RS-422 or RS-485. Feeding a true RS-232 source straight in will not work and can damage the port — use the matching level shifter on the carrier board, or an external transceiver if you genuinely need RS-232.
There is no native CAN/J1939 on the GNSS chip. CAN exists at product level (the HB3 above); on modules it is an external gateway/transceiver job. If a datasheet skim told you otherwise, you were reading the box, not the chip.
2. One port per job — the three-stream rule
Serial links usually fail because two logical jobs were pushed onto one port. Budget a port per job:
| Port | Job | Protocol | Typical rate |
|---|---|---|---|
| UART 1 | Autopilot position/status | NMEA 0183 out | 5-10 Hz |
| UART 2 | Corrections in (+ optional NMEA mirror out) | RTCM v3 / CMR in | as broadcast |
| UART 3 (HB10/HB59) | Raw logging, ROS driver, radio | SBF out | 1 Hz, up to 100 Hz bursts |
At absolute minimum, keep corrections on their own port. On a single-port build you have to multiplex RTCM input and NMEA output on the same UART — and a misconfiguration there looks exactly like a dead correction source, so you will spend an afternoon replacing cables for a config problem.
Dual-port wiring example:
Septentrio receiver Flight controller (Pixhawk class)
------------------- ---------------------------------
UART1 TX ----------------------> GPS1 RX
UART1 RX <---------------------- GPS1 TX
GND ----------------------- GND
5V ----------------------- 5V (dedicated UART 5V, not the servo rail)
Septentrio receiver NTRIP / telemetry link
------------------- ----------------------
UART2 RX <--- RTCM v3 corrections --- radio or companion TX
UART2 TX --- NMEA mirror (opt.) ---> radio or companion RX
3. Baud rate: match exactly, and know the headroom
-
115200 bps is the safe default, and what the parameter tables below assume (
GPS_BAUD_RATE=9on ArduPilot,GPS_1_BAUD=115200on PX4). - AsteRx-m3 Pro+ UARTs on HB10/HB59 support up to 4 Mbps. That headroom exists for raw multi-constellation SBF logging — full-band data will not fit in 115200.
- Change baud on one side at a time and re-verify. Changing both ends blind is how two working ports become one broken link.
- A baud mismatch typically shows as garbage or framing errors rather than a clean "no data" — which is exactly why it gets misdiagnosed as a dead receiver.
4. Protocol direction — the silent failure
This is the one that catches experienced people:
| Protocol | Direction | Carries | Goes to |
|---|---|---|---|
| NMEA 0183 | receiver -> autopilot | GGA/RMC position + status | GPS port |
| SBF | receiver -> logger/computer | PVTGeodetic, AttEuler, observables | SD card, ROS driver, post-processing |
| RTCM v2/v3 | base -> rover (IN) | corrections | rover's correction port |
| CMR v2.0 / CMR+ | base -> rover (IN) | corrections | rover's correction port |
NMEA and SBF are outputs you enable. RTCM/CMR is an input the rover must be told to accept. A receiver happily streaming perfect NMEA while ignoring its RTCM input will sit in Single forever with a healthy-looking data link — no error, no warning, nothing in a log to point at.
5. A script that names the fault
Guessing is what costs the afternoon, so here is a small checker. It reads the port for a few seconds and classifies what it sees: no bytes at all, bytes but nothing valid, binary SBF only, or NMEA with fix type, satellite count and age of differential corrections parsed straight out of GGA (fields 6, 7 and 13).
#!/usr/bin/env python3
"""Prove what a GNSS receiver is actually sending on a serial port.
Usage:
python gnss_stream_check.py /dev/ttyUSB0 115200
"""
import sys
import time
def parse_nmea(data: bytes) -> dict:
"""Pure parser: summarise an NMEA byte stream (no serial port needed)."""
out = {"bytes": len(data), "lines": 0, "gga": 0, "rmc": 0,
"fix": None, "sats": None, "age": None, "sbf_syncs": data.count(b"\xfa\xfa")}
for raw in data.split(b"\n"):
line = raw.strip()
if not line.startswith(b"$"):
continue
out["lines"] += 1
kind = line[3:6]
if kind == b"GGA":
out["gga"] += 1
f = line.split(b",")
if out["fix"] is None and len(f) > 13:
try:
out["fix"] = int(f[6]) if f[6] else 0 # 0 none, 1 GPS, 2 DGPS, 4 RTK fixed, 5 RTK float
out["sats"] = int(f[7]) if f[7] else 0
out["age"] = float(f[13]) if f[13] else None # age of differential corrections (s)
except ValueError:
pass
elif kind == b"RMC":
out["rmc"] += 1
return out
def read_port(port: str, baud: int, seconds: float) -> bytes:
import serial # pyserial, only needed for live capture
ser = serial.Serial(port, baud, timeout=1)
buf = b""
t0 = time.time()
while time.time() - t0 < seconds:
buf += ser.read(4096)
ser.close()
return buf
FIX_NAMES = {0: "no fix", 1: "GPS (single)", 2: "DGPS", 4: "RTK fixed", 5: "RTK float"}
def report(r: dict) -> None:
print(f"bytes={r['bytes']} lines={r['lines']} GGA={r['gga']} RMC={r['rmc']} "
f"SBF_syncs={r['sbf_syncs']}")
if r["bytes"] == 0:
print("-> No bytes at all: TX/RX swapped, wrong port, baud mismatch, "
"or receiver output disabled on this port.")
elif r["lines"] == 0 and r["sbf_syncs"] == 0:
print("-> Bytes but no valid NMEA or SBF: almost always a baud mismatch.")
elif r["gga"] == 0 and r["sbf_syncs"] > 0:
print("-> Binary SBF only on this port. Fine for logging, but the autopilot "
"needs NMEA on its port.")
if r["fix"] is not None:
print(f"-> fix={r['fix']} ({FIX_NAMES.get(r['fix'], '?')}) sats={r['sats']} "
f"correction_age={r['age']}s")
if r["fix"] in (0, 1) and r["age"] in (None, 0.0):
print(" Never RTK: corrections are not reaching the receiver "
"(check RTCM is wired IN, not OUT).")
elif r["age"] is not None and r["age"] > 10:
print(" Corrections are arriving but stale (>10 s): link/caster problem, "
"not receiver.")
if __name__ == "__main__":
port = sys.argv[1] if len(sys.argv) > 1 else "/dev/ttyUSB0"
baud = int(sys.argv[2]) if len(sys.argv) > 2 else 115200
report(parse_nmea(read_port(port, baud, 5)))
The classifier has four outcomes. Exercised against synthetic NMEA and SBF streams, they print like this:
# healthy stream: GGA quality 4 = RTK fixed, fresh corrections
bytes=149 lines=2 GGA=1 RMC=1 SBF_syncs=0
-> fix=4 (RTK fixed) sats=18 correction_age=1.2s
# bytes arriving, but nothing valid -> baud mismatch
bytes=200 lines=0 GGA=0 RMC=0 SBF_syncs=0
-> Bytes but no valid NMEA or SBF: almost always a baud mismatch.
# receiver configured for raw logging only
bytes=56 lines=0 GGA=0 RMC=0 SBF_syncs=2
-> Binary SBF only on this port. Fine for logging, but the autopilot needs NMEA on its port.
# NMEA fine, but corrections never arrive
-> fix=1 (GPS (single)) sats=9 correction_age=None
Never RTK: corrections are not reaching the receiver (check RTCM is wired IN, not OUT).
That last branch is the important one. NMEA GGA fix quality is 4 = RTK fixed, 5 = RTK float, 1 = single, 0 = no fix. And a correction_age above ~10 s means corrections are arriving but are stale — a link or caster problem, not a receiver problem. Two numbers, and the whole "is it the radio or the receiver" argument is over.
6. Autopilot parameters
ArduPilot:
| Parameter | Value | Notes |
|---|---|---|
GPS_TYPE |
9 | Septentrio |
GPS_BAUD_RATE |
9 | 115200 |
GPS_RATE_MS |
100 | 10 Hz |
GPS_AUTO_CONFIG |
1 | Receiver configured on every boot |
GPS2_TYPE / GPS2_BAUD_RATE
|
9 / 9 | Second receiver or dual-antenna heading |
PX4:
| Parameter | Value | Notes |
|---|---|---|
GPS_1_GNSS_ID |
1 | Septentrio |
GPS_1_CONFIG |
TELEM2 | Serial port |
GPS_1_BAUD |
115200 | Must match receiver |
GPS_1_PROTOCOL |
14 | Septentrio SBF/NMEA (autodetect usually works) |
One driver per port. Do not point two autopilot drivers — or an autopilot plus a companion logging script — at the same UART. They fight over the stream and both look intermittent.
7. Fault isolation table
| Symptom | Likely cause |
|---|---|
| No data at all | TX/RX swapped, wrong port, baud mismatch |
| Garbage / framing errors | Baud or protocol mismatch on that port |
| 3D fix but never RTK Fixed | Corrections not arriving, or RTCM directed OUT instead of IN |
| Fixed -> float, recovers when you move | Multipath (canopy, buildings, vehicle body) — antenna placement |
| Fix lost near power lines, fences, 4G sites | In-band RF interference |
| Data dies at high output rate | Baud too low for the stream set — reduce blocks or raise baud |
| Two receivers conflict / intermittent position | Two drivers on one UART, or mismatched GPS2_*
|
| Resets to defaults after power cycle | Configuration not stored as boot config |
8. Why a perfectly configured port can still lose the fix
A port can be flawless and the fix still drops, because the receiver's front end is being desensitised. Power lines, electric fences, 4G/5G sites and deliberate jamming all put energy in band. AIM+ on our receivers rejects roughly 40-60 dB of in-band interference versus ~25 dB typical for consumer GNSS modules, and it runs continuously with no configuration.
The tell is the shape of the failure: if C/N0 sags across many satellites at once and the drop correlates with position rather than sky view, your link is fine and the RF environment is not. If only low-elevation satellites fade and it recovers as soon as you clear the trees, that is multipath — an antenna placement problem, not a receiver one.
The full port/baud/protocol guide plus an SBF log parser and a synthetic SBF generator (handy for testing a pipeline without driving to a field) live in the repo: septentrio-gnss-integration-guide.
More on the hardware: GNSS receivers | AIM+ resilient GNSS | integration guides
How do you allocate your ports? Do you keep corrections on a dedicated UART, or multiplex RTCM and NMEA on one link — and does your correction age stay steady through a whole pass?
Top comments (0)