RTK Carrier Solution Stuck in Floating on ZED-F9P/NEO-D9S combo board

Hello,

I am working on setting up a ZED-F9P/ NEO-D9S combo board with an Artemis Nano redboard connected with the qwiik connect system. The antenna I am using is an SPK-6E L1/L2/L5 band and I have a PointPerfect account set up for L-Band SPARTN messages. For my application, I am particularly interested in high vertical position accuracy.

I am able to get a floating carrier solution with an accuracy that is high enough for my application but the carrier solution never reaches a fixed carrier solution. The spartn messages are being corrected decrypted and picked up and I have let it sit still in this configuration for 20 minutes without ever reaching a fixed solution. Is there a setting that I am missing? Should I use a different antenna? Do you have a suggestion for the most robust antenna we can use for high vertical accuracy? The Arduino code I am using is pasted below here with the exception of the spartan keys. Any help is much appreciated thank you!

#include <SparkFun_u-blox_GNSS_v3.h> //http://librarymanager/All#SparkFun_u-blox_GNSS_v3
SFE_UBLOX_GNSS myLBand; // NEO-D9S
SFE_UBLOX_GNSS myGNSS; // ZED-F9P

#include “spartn_keys.h”

void pushRXMPMP(UBX_RXM_PMP_message_data_t *pmpData)
{
//Extract the raw message payload length
uint16_t payloadLen = ((uint16_t)pmpData->lengthMSB << 8) | (uint16_t)pmpData->lengthLSB;
Serial.print(F("New RXM-PMP data received. Message payload length is "));
Serial.print(payloadLen);

#ifndef noPush

Serial.println(F(" Bytes. Pushing it to the GNSS…"));

//Push the PMP data to the GNSS
//The payload length could be variable, so we need to push the header and payload, then checksum
myGNSS.pushRawData(&pmpData->sync1, (size_t)payloadLen + 6); // Push the sync chars, class, ID, length and payload
myGNSS.pushRawData(&pmpData->checksumA, (size_t)2); // Push the checksum bytes

#else

Serial.println(F(" Bytes."));

#endif

}

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// Callback: printPVTdata will be called when new NAV PVT data arrives
// See u-blox_structs.h for the full definition of UBX_NAV_PVT_data_t
// _____ You can use any name you like for the callback. Use the same name when you call setAutoPVTcallbackPtr
// / _____ This must be UBX_NAV_PVT_data_t
// | / _____ You can use any name you like for the struct
// | | /
// | | |
void printPVTdata(UBX_NAV_PVT_data_t *ubxDataStruct)
{
double latitude = ubxDataStruct->lat; // Print the latitude
Serial.print(F("Lat: "));
Serial.print(latitude / 10000000.0, 7);

double longitude = ubxDataStruct->lon; // Print the longitude
Serial.print(F(" Long: "));
Serial.print(longitude / 10000000.0, 7);

double altitude = ubxDataStruct->hMSL; // Print the height above mean sea level
Serial.print(F(" Height: "));
Serial.print(altitude / 1000.0, 3);

uint8_t fixType = ubxDataStruct->fixType; // Print the fix type
Serial.print(F(" Fix: “));
Serial.print(fixType);
if (fixType == 0)
Serial.print(F(” (None)“));
else if (fixType == 1)
Serial.print(F(” (Dead Reckoning)“));
else if (fixType == 2)
Serial.print(F(” (2D)“));
else if (fixType == 3)
Serial.print(F(” (3D)“));
else if (fixType == 4)
Serial.print(F(” (GNSS + Dead Reckoning)“));
else if (fixType == 5)
Serial.print(F(” (Time Only)“));
else
Serial.print(F(” (UNKNOWN)"));

uint8_t carrSoln = ubxDataStruct->flags.bits.carrSoln; // Print the carrier solution
Serial.print(F(" Carrier Solution: “));
Serial.print(carrSoln);
if (carrSoln == 0)
Serial.print(F(” (None)“));
else if (carrSoln == 1)
Serial.print(F(” (Floating)“));
else if (carrSoln == 2)
Serial.print(F(” (Fixed)“));
else
Serial.print(F(” (UNKNOWN)"));

uint32_t hAcc = ubxDataStruct->hAcc; // Print the horizontal accuracy estimate
Serial.print(F(" Horizontal Accuracy Estimate: “));
Serial.print(hAcc);
Serial.print(F(” (mm)"));

uint32_t vAcc = ubxDataStruct->vAcc; // Print the horizontal accuracy estimate
Serial.print(F(" Vertical Accuracy Estimate: “));
Serial.print(vAcc);
Serial.print(F(” (mm)"));

Serial.println();
}
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// Callback: printRXMCOR will be called when new RXM COR data arrives
// See u-blox_structs.h for the full definition of UBX_RXM_COR_data_t
// _____ You can use any name you like for the callback. Use the same name when you call setRXMCORcallbackPtr
// / _____ This must be UBX_RXM_COR_data_t
// | / _____ You can use any name you like for the struct
// | | /
// | | |
void printRXMCOR(UBX_RXM_COR_data_t *ubxDataStruct)
{
Serial.print(F("UBX-RXM-COR: ebno: "));
Serial.print((double)ubxDataStruct->ebno / 8, 3); //Convert ebno to dB

Serial.print(F(" protocol: "));
if (ubxDataStruct->statusInfo.bits.protocol == 1)
Serial.print(F(“RTCM3”));
else if (ubxDataStruct->statusInfo.bits.protocol == 2)
Serial.print(F(“SPARTN”));
else if (ubxDataStruct->statusInfo.bits.protocol == 29)
Serial.print(F(“PMP (SPARTN)”));
else if (ubxDataStruct->statusInfo.bits.protocol == 30)
Serial.print(F(“QZSSL6”));
else
Serial.print(F(“Unknown”));

Serial.print(F(" errStatus: "));
if (ubxDataStruct->statusInfo.bits.errStatus == 1)
Serial.print(F(“Error-free”));
else if (ubxDataStruct->statusInfo.bits.errStatus == 2)
Serial.print(F(“Erroneous”));
else
Serial.print(F(“Unknown”));

Serial.print(F(" msgUsed: "));
if (ubxDataStruct->statusInfo.bits.msgUsed == 1)
Serial.print(F(“Not used”));
else if (ubxDataStruct->statusInfo.bits.msgUsed == 2)
Serial.print(F(“Used”));
else
Serial.print(F(“Unknown”));

Serial.print(F(" msgEncrypted: "));
if (ubxDataStruct->statusInfo.bits.msgEncrypted == 1)
Serial.print(F(“Not encrypted”));
else if (ubxDataStruct->statusInfo.bits.msgEncrypted == 2)
Serial.print(F(“Encrypted”));
else
Serial.print(F(“Unknown”));

Serial.print(F(" msgDecrypted: "));
if (ubxDataStruct->statusInfo.bits.msgDecrypted == 1)
Serial.print(F(“Not decrypted”));
else if (ubxDataStruct->statusInfo.bits.msgDecrypted == 2)
Serial.print(F(“Successfully decrypted”));
else
Serial.print(F(“Unknown”));

Serial.println();
}

#define OK(ok) (ok ? F(" → OK") : F(" → ERROR!")) // Convert uint8_t into OK/ERROR

void setup() {

Wire.begin(); //Start I2C

// -------------- Configure NEO-D9S --------------
while (myLBand.begin(Wire, 0x43) == false) //Connect to the u-blox NEO-D9S using Wire port. The D9S default I2C address is 0x43 (not 0x42)
{
Serial.println(F(“u-blox NEO-D9S not detected at default I2C address. Please check wiring.”));
delay(2000);
}
Serial.println(F(“u-blox NEO-D9S connected”));

// myLBand.enableDebugging(); // Uncomment this line to enable helpful debug messages on Serial

// L-Band center frequency
//const uint32_t myLBandFreq = 1556290000; // Uncomment this line to use the US SPARTN 1.8 service
const uint32_t myLBandFreq = 1545260000; // Uncomment this line to use the EU SPARTN 1.8 service

// NEO-D9S Configuration Settings
Serial.println(F(“u-blox NEO-D9S connected”));

myLBand.newCfgValset(); // Create a new Configuration Interface message - this defaults to VAL_LAYER_RAM_BBR (change in RAM and BBR)
myLBand.addCfgValset(UBLOX_CFG_PMP_CENTER_FREQUENCY, myLBandFreq); // Default 1539812500 Hz
myLBand.addCfgValset(UBLOX_CFG_PMP_SEARCH_WINDOW, 2200); // Default 2200 Hz
myLBand.addCfgValset(UBLOX_CFG_PMP_USE_SERVICE_ID, 1); // Default 1
myLBand.addCfgValset(UBLOX_CFG_PMP_SERVICE_ID, 21845); // Default 50821
myLBand.addCfgValset(UBLOX_CFG_PMP_DATA_RATE, 2400); // Default 2400 bps
myLBand.addCfgValset(UBLOX_CFG_PMP_USE_DESCRAMBLER, 1); // Default 1
myLBand.addCfgValset(UBLOX_CFG_PMP_DESCRAMBLER_INIT, 26969); // Default 23560
myLBand.addCfgValset(UBLOX_CFG_PMP_USE_PRESCRAMBLING, 0); // Default 0
myLBand.addCfgValset(UBLOX_CFG_PMP_UNIQUE_WORD, 16238547128276412563ull);
// myLBand.addCfgValset(UBLOX_CFG_MSGOUT_UBX_RXM_PMP_I2C, 1); // Ensure UBX-RXM-PMP is enabled on the I2C port
// myLBand.addCfgValset(UBLOX_CFG_MSGOUT_UBX_RXM_PMP_UART1, 1); // Output UBX-RXM-PMP on UART1
myLBand.addCfgValset(UBLOX_CFG_UART2OUTPROT_UBX, 1); // Enable UBX output on UART2
myLBand.addCfgValset(UBLOX_CFG_MSGOUT_UBX_RXM_PMP_UART2, 1); // Output UBX-RXM-PMP on UART2
// myLBand.addCfgValset(UBLOX_CFG_UART1_BAUDRATE, 38400); // match baudrate with ZED default
myLBand.addCfgValset(UBLOX_CFG_UART2_BAUDRATE, 38400); // match baudrate with ZED default
uint8_t ok = myLBand.sendCfgValset(); // Apply the settings

Serial.print(F("L-Band: configuration "));
if (ok)
Serial.println(F(“OK”));
else
Serial.println(F(“NOT OK!”));

myLBand.softwareResetGNSSOnly(); // Restart the NEO-D9S to apply the configuration settings
myLBand.setRXMPMPmessageCallbackPtr(&pushRXMPMP); // Call pushRXMPMP when new PMP data arrives. Push it to the GNSS

// -------------- Configure ZED-F9P --------------
while (myGNSS.begin() == false) //Connect to the u-blox module using Wire port
{
Serial.println(F(“u-blox GNSS module not detected at default I2C address. Please check wiring.”));
delay(2000);
}
Serial.println(F(“u-blox GNSS module connected”));

//Check the ZED firmware version - SPARTN is only supported on ZED-F9P from HPG 1.30 and ZED-F9R from HPS 1.21 onwards
if (myGNSS.getModuleInfo())
{
Serial.print(F(“FWVER: “));
Serial.print(myGNSS.getFirmwareVersionHigh()); // Returns uint8_t
Serial.print(F(”.”));
Serial.println(myGNSS.getFirmwareVersionLow()); // Returns uint8_t

Serial.print(F("Firmware: "));
Serial.println(myGNSS.getFirmwareType()); // Returns HPG, SPG etc. as (const char *)

if (strcmp(myGNSS.getFirmwareType(), "HPG") == 0)
  if ((myGNSS.getFirmwareVersionHigh() == 1) && (myGNSS.getFirmwareVersionLow() < 30))
    Serial.println("Your module is running old firmware which may not support SPARTN. Please upgrade.");
    
if (strcmp(myGNSS.getFirmwareType(), "HPS") == 0)
  if ((myGNSS.getFirmwareVersionHigh() == 1) && (myGNSS.getFirmwareVersionLow() < 21))
    Serial.println("Your module is running old firmware which may not support SPARTN. Please upgrade.");

}
else
Serial.println(F(“Error: could not read module info!”));

// myGNSS.enableDebugging(); // Uncomment this line to enable helpful debug messages on Serial

// Configure UART2 Port on ZED-F9P to accept PMP Correction Data
ok = myGNSS.setI2COutput(COM_TYPE_UBX); //Turn off NMEA noise
// if (ok) ok = myGNSS.setI2CInput(COM_TYPE_UBX | COM_TYPE_NMEA | COM_TYPE_SPARTN); //Be sure SPARTN input is enabled
if (ok) ok = myGNSS.setUART2Input(COM_TYPE_UBX | COM_TYPE_NMEA | COM_TYPE_SPARTN); //Be sure SPARTN input is enabled
// if (ok) ok = myGNSS.setDGNSSConfiguration(SFE_UBLOX_DGNSS_MODE_FIXED); // Set the differential mode - ambiguities are fixed whenever possible
// if (ok) ok = myGNSS.setNavigationFrequency(1); //Set output in Hz.
// if (ok) ok = myGNSS.setVal8(UBLOX_CFG_MSGOUT_UBX_RXM_COR_I2C, 1); // Enable UBX-RXM-COR messages on I2C
// if (ok) ok = myGNSS.setUART2Input(COM_PORT_UART2, COM_TYPE_UBX | COM_TYPE_NMEA | COM_TYPE_SPARTN); //Be sure SPARTN input is enabled
if (ok) ok = myGNSS.setDGNSSConfiguration(SFE_UBLOX_DGNSS_MODE_FIXED); // Set the differential mode - ambiguities are fixed whenever possible
if (ok) ok = myGNSS.setVal8(UBLOX_CFG_SPARTN_USE_SOURCE, 1); // use LBAND PMP message
// if (ok) ok = myGNSS.setVal8(UBLOX_CFG_NAVSPG_CONSTR_DGNSSTO, 90); // set interval for how long to wait between correction messages
if (ok) ok = myGNSS.setVal8(UBX_NAV_PVT, 1);

//Configure the SPARTN IP Dynamic Keys
//“When the receiver boots, the host should send ‘current’ and ‘next’ keys in one message.” - Use setDynamicSPARTNKeys for this.
//“Every time the ‘current’ key is expired, ‘next’ takes its place.”
//“Therefore the host should then retrieve the new ‘next’ key and send only that.” - Use setDynamicSPARTNKey for this.
// The key can be provided in binary (uint8_t) format or in ASCII Hex (char) format, but in both cases keyLengthBytes must represent the binary key length in bytes.
if (ok) ok = myGNSS.setDynamicSPARTNKeys(currentKeyLengthBytes, currentKeyGPSWeek, currentKeyGPSToW, currentDynamicKey,
nextKeyLengthBytes, nextKeyGPSWeek, nextKeyGPSToW, nextDynamicKey);
myGNSS.setAutoPVTcallbackPtr(&printPVTdata); // Enable automatic NAV PVT messages with callback to printPVTdata so we can watch the carrier solution go to fixed
myGNSS.setRXMCORcallbackPtr(&printRXMCOR); // Print the contents of UBX-RXM-COR messages so we can check if the PMP data is being decrypted successfully

}

void loop() {
myGNSS.checkUblox(); // Check for the arrival of new GNSS data and process it.
myGNSS.checkCallbacks(); // Check if any GNSS callbacks are waiting to be processed.

myLBand.checkUblox(); // Check for the arrival of new PMP data and process it.
myLBand.checkCallbacks(); // Check if any LBand callbacks are waiting to be processed.
}

I personally wouldn’t have any sort of expectation of vertical accuracy without a RTK fix.
Even with a Fixed Solution, the estimated uncertainty is just an estimate.
Repeatability starts to give you confidence in the precision of the position, but not specifically accuracy.

Staying in Float for 20 minutes could be the result of locality (multipath), or simply poor satellite geometry at that time for your location. That’s not uncommon at times.

Helical antennas don’t have the benefit of a true ground plane, but I suppose you could try a DIY solution to block reflections from multipath signals and see if you notice a difference.
@clive1 would be a great resource, if he has time to drop in.

Or if you are interested in testing a different antenna, the TOP106 is a proven performer for the money.

Or this one for L5.

Note: I have nothing against the helical antenna you linked to.

If you don’t mind sharing, what is your target goal for Vertical Accuracy?

Hey rftop,

This is great info, thank you! I will try to put it out again today and see if it will reach a fixed state with some different constellation geometry. I think we will also upgrade to the L1/L2/L5 antenna you suggested and see it it can improve our results.

Our target vertical accuracy is 10 cm but 20 cm would be ok as well. We are planning to include this on a buoy and so it will be on a dynamic frame and ear the surface of the water so there may be more potential for multipathing there, so a better antenna with a grounding plate is a good idea!

It would be great to talk some more and if @clive1 would be. a good resource as well maybe we could chat some more. Thanks for your help and I will keep you updated as we work with it some more. Thanks!

Well you could put a ground plane behind a helical antenna, but you’re usually using one to reduce the mass, so might not be entirely practical.

Indirect signals especial from the ground, water, or close in reflective surfaces are the things causing most issues. By buildings it’s the physical masking which becomes a big issue.

I’m not particular a fan of volute antennas, this comes from one of my mentors, who observed that they were overly directional, and the phase-center wasn’t focused. Most satellites are not directly above you, and perfectly polarized. You typically don’t see survey grade ones with ANTEX files. Perhaps someone can dig up some measurements with the GEO+ robot antenna mapping gear.

Staying in Float is something you see with unstable/noisy measurements, where over time (near-term) it can’t resolve to one carrier cycle ~19cm L1, or that it can’t solidly resolve L1 and L2.
L1-Only RTK typically had around a 7.5 minutes ambiguity resolution time (migrate from float to fixed), whereas multi-band this dropped closer to 7.5 seconds. Consequently losing carrier lock is a much bigger problem to recover from.

Watch for air flow / drafts over receiver, this can be very disruptive, causing loss of carrier lock across all tracked satellites. Seal in an air-tight box/case, conformal coat, or pot.

Perhaps look for Marine antenna’s, Furuno used to have L1 GPS antennas with a 14-15 cm type diameter ground planes, not sure what kind of GNSS / multi-band antennas are available within that market segment these days.

u-Blox has just released an new ANN-MB2, that might be worth a review, calls for a 12 cm diameter plane/plate.

Beitian has several “four star full frequency” antenna, say BT-800S or BT-280, which should be suitable for a F9P/D9S combo, but not sure of IP rating or suitability for a marine application.

2 Likes

The F9P anticipates a direct feed thru of the UBX-RXM-PMP messages. Doesn’t the combo design permit direct connectivity via UART2, without inter-staging via the MCU?

You’d perhaps also accumulate long-term metrics / statistics with respect to the error/success reporting as glancing at instantaneous reporting doesn’t tell much of a story about what’s going on.

For robust analysis, my approach would be to recover the SPARTN packets out of the D9S UBX-RXM-PMP packets and do the packet reception and integrity check of those, before handing off to the F9P. You don’t have to decrypt them, as the CRC24 is over the data encapsulated in the encrypted form, so they can be validated. Anyway metrics and statistics from the data output from the D9S would be significantly more helpful to get a clear understanding about what’s happening there.

You really need mid 40’s dBHz to demodulate cleanly, and the satellite is geo-stationary, so a directional antenna at it generally works markedly better.

I’ve discussed the UBX-MON-PMP message on my GitHub page, this provides frequency, offset, lock status, lock time and dBHz reporting.

With the OSNMA for example, what I found most helpful was the long-term lock vs unlock percentage, current lock time, worst case unlocked time, and total run time hours. And secondarily the usage in UBX-NAV-SIG

1 Like