UBlox ZED-F9R observations

mborgerson

Well-known member
A few weeks before Christmas, a friend contacted me about putting together a program to log position and IMU data from a ZED-F9R to help a friend of his analyze his performance as an amateur driver of a Porsche 911 on tracks in their area. The goal is to collect latitude, longitude, speed, XYZ gyro rates and XYZ acceleration values. He would like to collect the data at 25 samples per second and log it to an SD card for offline analysis using Matlab. A quick look at the ZED-F9R specs indicated that the module should be capable of delivering data at that rate.

I decided to use a T4.1 to collect the data using the Sparkfun-ublox_GNSS_Arduino_Library, since I had a Sparkfun board on loan for the project. After all, why reinvent the wheel if not necessary? What I didn't realize was exactly how big that wheel really was!

Library structure definitions: 2789 lines
Library .h header file: 1805 lines
Library.cpp source: 18,459 lines

A lot of the source lines are in excess of requirements for my project. I don't use all the debugging output, the RTK stuff and the helper functions that reduce the learning curve for Arduino programmers with little or no experience in decoding binary data streams. I also have a longstanding aversion to allocating data structures on the heap, which the library does in an attempt to reduce RAM overhead by only allocating structures that are actually used in your particular program. (The library does check for a NULL pointer each time before accessing a heap-based structure.) The allocation as required canb help a lot on board with less memory than a T4.1.

I then understood why my friend considered the logging program a bit beyond the capabilities of any of his nearby friends (I'm in Corvallis, Oregon and he is in Port Townsend, Washington.)

The ZED-F9R is a complex module: It handles just about every navigation satellite system, Real-Time-Kinematics, SBAS, etc. etc. It also does dead reckoning during loss of satellite signals. For the initial version of the logger, I don't need all those things. I need GNSS output at 25Hz with 100cm resolution and 2-meter accuracy. I also need IMU data at that same rate. I know the T4.1 can collect and log the data at the needed rate: 25 * 64 ,or 1600 bytes per second in a stream of binary packets collected into a buffer of about 8KB for efficient SD card writes.

After a few days studying the Sparkfun library and the ZED setup commands and UBX binary packet specifications, I put together a simple logger in a few more days. After a few days of tests, I started seeing some puzzling results.

1. The 1mAh rechargeable backup battery on the ZED module won't backup the GNN for hot starts for more than a couple of hours from a full charge. The battery charges at about 30uA and discharges at about 50uA. A couple of hours is OK for hot or warm starts, but can't depend on the configurations in the battery-backed-RAM to last overnight if the Sparkfun module is not connected to a 5V supply.

2. The UBlox manuals and U-Center software say that you can set the fix update interval to as low as 25 milliseconds, or 40Hz. I wanted 40 milliseconds for 25Hz. However, if you try to configure the ZED-F9R at that rate it doesn't stick. The actual minimum fix interval seems to be 50 milliseconds.

3. You can get position update packets at 25Hz by setting the priority navigation rate to 25Hz. That gives priority to certain NAV message, among them the NAV_PVT messages I am using. These delivered packets are apparently generated from the 20Hz actual computed fixes. I've found no good explanation of exactly how that is done, but I suspect it is the output of an internal Kalman filter working with the GNSS data and the IMU data.

4. If you try to do 20Hz fixes and 25Hz output using all the available satellite constellations, the overworked MPU inside the ZED seems to give up in surprising ways. (And not nice surprises!). I've seen many instances of the ZED either resetting to factory defaults, or losing some or all of the setup information I've sent to the module. This often means the Teensy stops collecting as the baud rate on the ZED UART output has changed and packets can no longer be decoded. This particular problem seems to be eliminated if I restrict the ZED to using just the GPS constellation and SBAS. It's not surprising that it can better cope with just eight satellites in view that with 24 satellites.

5. It may be possible to use more satellite constellations by reducing the fix rate below 20Hz. Perhaps that will reduce the ZED MPU load enough so that it can keep up with the extrapolated 25Hz output.

I'm in the process of writing a 'training wheel' subset of the Sparkfun library to handle just the NAV_PVT and ESF_MEAS packets I need for the logger. As a first step, I wrote a simple UBX packet parser that runs in about 40 lines of code. I then added functions to examine the collected packets and extract the required data from the UBX_NAV_PVT and UBX_ESF_MEAS packets while ignoring any other packets. That took about another hundred lines. I cribbed shamelessly from the Sparkfun library's structure definitions--which added up to another 70 or 80 lines of source. There will be more work to be done if I discover the need to handle other packets. For now, I'm sticking with a minimalist approach.


As a teaser here is a minimalist test program for the UBX packet parser: (note: functions to break out the data are not included.)

Code:
// Simple demo to collect UBX packets.   This demo assumes that
// the UBlox ZED-F9R module has been set up to automatically send NAV_PVT
// and ESF_MEAS ppackets and that the ZED UART1 baud rate has been set
// to 230400 Baud.  May require that UART1 NMEA packets have been disabled
// for higher data rates.
// MJB 1/8/2023

const char compileTime[] = " Compiled on " __DATE__ " " __TIME__;

#define gpsPort Serial1

// Define the three status results from the parser
#define STATEMPTY 0
#define STATFILLED 1
#define STATERROR 2

#define PAYLOADMAX 248
typedef struct __attribute__((aligned(4))) {
  uint32_t status;
  uint8_t pktclass;
  uint8_t pktid;
  uint8_t lenlow;        // payload length low byte
  uint8_t lenhi;         // payload length low byte
  uint8_t payload[PAYLOADMAX];  // total is 256 bytes
} ubxPacket_t;

ubxPacket_t myPacket;
 
elapsedMillis showDataTimer;
elapsedMicros parseTimer;
void setup() {
  // put your setup code here, to run once:

  Serial.begin(115200);
  // while (!Serial); //Wait for user to open terminal
  delay(1000);

  Serial.printf("\n\nUBlox Parser Test %s \n", compileTime);
  delay(100);

  showDataTimer = 0;
  parseTimer = 0;
  gpsPort.begin(230400);  // Open GPS hardware serial port

  myPacket.status = STATEMPTY; // initialize packet status
}

// Loop calls the parser every 300uSec the shows statistics on
// packets collected
void loop() {
  if(parseTimer > 300){
    parseTimer = 0;
    ParsePacketInput();
  }
  if (showDataTimer > 1000) {
      ShowStats();
      showDataTimer = 0;
  }
    // handle the packet if it is complete or there was an error
  if(myPacket.status != STATEMPTY) HandlePacket();
}

// constants that identify packets received
const uint8_t UBX_CLASS_NAV = 0x01;
const uint8_t UBX_CLASS_ESF = 0x10;
const uint8_t UBX_NAV_PVT = 0x07;
const uint8_t UBX_ESF_MEAS = 0x02;

uint32_t  meascount, pvtcount, errcount;  // for statistics output
uint16_t pldlength;

//  Check parser packet status and react to filled packets
void HandlePacket(void) {
  if (myPacket.status == STATERROR) {
    errcount++;
    pldlength = myPacket.lenlow + (myPacket.lenhi << 8);
    myPacket.status = STATEMPTY; // continue after error
  } else if (myPacket.status == STATFILLED) {
    if (myPacket.pktclass == UBX_CLASS_NAV) {
      if (myPacket.pktid == UBX_NAV_PVT){
        pvtcount++;
        //copyPVTdata(&myPacket); This is where you would save or display the data
      }
    }
    if (myPacket.pktclass == UBX_CLASS_ESF) {
      if (myPacket.pktid == UBX_ESF_MEAS){
        meascount++; 
        //copyESFdata(&myPacket); This is where you would save or display the data
      }
    }
    myPacket.status = STATEMPTY; // mark as empty to resume parsing
  }  // end of if(myPacket.status == STATFILLED)

}




/*************************************************************
  Packet Input and checksum
*****************************************************************/
volatile uint8_t ck_a, ck_b;  // accumulated during reception
volatile uint8_t cksumA, cksumB; // read after end of payload
inline void add2Cksum() __attribute__((always_inline));
void add2Cksum(uint8_t pbt) {
  ck_a += pbt;
  ck_b += ck_a;
}

bool goodCksum(uint8_t pbA, uint8_t pbB) {
  return ((pbA == ck_a) && (pbB == ck_b));
}
void initCksum(void) {
  ck_a = 0;
  ck_b = 0;
}

// UBX Packet Parser Called from loop.  Reads from serial gpsPort
// reads while serial data available or until packet is complete.
void ParsePacketInput(void) {
  ubxPacket_t *pptr;
  pptr = &myPacket; // looking forward to using a queue of packets
  uint8_t pbyte;
  // variables retained between calls for state machine
  static uint16_t pldcount;
  static uint16_t pldsize;
  static uint16_t pstate;

  while (gpsPort.available() && (myPacket.status == STATEMPTY)) {
    pbyte = gpsPort.read();
    switch (pstate) {
      case 0:  // check for 0xB5  first preamble byte
        if (pbyte == 0xB5) pstate = 1;
        break;
      case 1:  // check for 0x62 second preamble byte
        if (pbyte == 0x62) pstate = 2;
        else pstate = 0; // back to start if not proper preamble
        break;
      case 2:  // get pktclass
        initCksum();  // reset checksum
        add2Cksum(pbyte); // add the first byte to checksum
        pptr->pktclass = pbyte; // save packet class
        pstate = 3;
        break;
      case 3:  // get pktid
        add2Cksum(pbyte);
        pptr->pktid = pbyte;
        pstate = 4;
        break;
      case 4:  // get len low byte
        pptr->lenlow = pbyte;
        pstate = 5;
        add2Cksum(pbyte);
        pldsize = pbyte; // save incoming payload length for state 7
        break;
      case 5:  // get lenhi
        pptr->lenhi = pbyte;
        pstate = 6;
        pldcount = 0;
        add2Cksum(pbyte);
        pldsize += ((uint16_t)pbyte << 8);
        break;
      case 6:  // get payload until pldcount >= pldsize;
        pptr->payload[pldcount] = pbyte;
        add2Cksum(pbyte);
        pldcount++;
        if (pldcount >= pldsize) pstate = 7; // next state when payload full
        if (pldcount > PAYLOADMAX ){ // exit with error before buffer overrun
          pptr->status = STATERROR;
        }
        break;
      case 7:  // read first cksum byte
        cksumA = pbyte;
        pstate = 8;
        break;
      case 8:  // read 2nd cksum byte
        cksumB = pbyte;
        if (goodCksum(cksumA, cksumB)) {
          pptr->status = STATFILLED;
        } else {
          pptr->status = STATERROR;
        }
        pstate = 0;  // reset state for next packet
        break;
    }  // end of switch(pstate)
  }    // end of while (gpsPort->available())
}

// Show the number of packets received of each type, including those with checksum errors
void ShowStats(void) {
  Serial.printf("PVT:%6lu   MEAS:%6lu  Error:%6lu \n",  pvtcount,   meascount, errcount);
}
 
Specs say 14mm accuracy. Do you get anywhere near that?

I think that specification is probably for a system using real-time correction input from a base station.

Then there's the issue of accuracy vs. precision. When I plot the position of my jeep parked in my driveway, the GPS position with WAAS on agrees with the Google Earth aerial photo to within a meter or so. When I'm driving in a straight line, the plot deviates from the mean path by about +/- 20cm. So I figure I'm getting ~1meter accuracy and 20cm precision.
 
Hi you may want to check out my uBlox lib:
https://github.com/bolderflight/ublox

It's lighter weight than SparkFun; although, you do need to config the receiver yourself on u-center.


You library looks like what I need. It handles a greater variety of packet types and output data than does my minimalist code.

I have extended my example code to handle the collection and logging of the data, as well as sending setup commands to the ZED. The whole program now come to about 1000 lines. I think if I use your library I can cut it about in half.

Here is the code I use to set up the ZED by writing commands into the RAM:
Code:
// Functions to Send commands based on strings
// produced by U-center
#include <string.h>
//CFG-RATE-MEAS (0x30210001)  to set rate-meas to 50mSec 20Hz
const char cmstrR50[] =     "b5 62 06 8a 0a 00 00 01 00 00 01 00 21 30 32 00 1f 55";  // len 53

//CFG-RATE-MEAS (0x30210001)  to set rate-meas to 100mSec 10 Hz
const char cmstrR100[] =    "b5 62 06 8a 0a 00 00 01 00 00 01 00 21 30 64 00 51 b9";

//CFG-RATE-MEAS (0x30210001)  to set rate-meas to 80mSec 12.5Hz
const char cmstrR80[] =     "b5 62 06 8a 0a 00 00 01 00 00 01 00 21 30 50 00 3d 91";

//CFG-UART1OUTPROT-NMEA (0x10740002)  to shut off NMEA on UART1
const char cmstrNoNMEA[] =    "b5 62 06 8a 09 00 00 01 00 00 02 00 74 10 00 20 b7"; 

const char cmstrESFON[]  =    "b5 62 06 8a 09 00 00 01 00 00 01 00 08 10 01 b4 6f";
const char cmstrESFOFF[] =    "b5 62 06 8a 09 00 00 01 00 00 01 00 08 10 00 b3 6e";

const char cmstrINSOFF[]  =   "b5 62 06 8a 09 00 00 01 00 00 15 01 91 20 00 61 91";  
const char cmstrINSON[]   =   "b5 62 06 8a 09 00 00 01 00 00 15 01 91 20 01 62 92";  

const char cmstrMEASOFF[]  =  "b5 62 06 8a 09 00 00 01 00 00 78 02 91 20 00 c5 84";  
const char cmstrMEASON[]   =  "b5 62 06 8a 09 00 00 01 00 00 78 02 91 20 02 c7 86";  // sets rate 2

const char cmstrPVTON[]    =  "b5 62 06 8a 09 00 00 01 00 00 07 00 91 20 01 53 48";


// Send commands to set up ZED RAM layer configuration
void SetupZED(void){
  Serial.println("Sending setup commands to ZED-F9R");
  uint16_t jumperval = 0;
  if (digitalRead(pinJmp1) == 0) jumperval = 1;
  if (digitalRead(pinJmp2) == 0) jumperval += 2;
  if (digitalRead(pinJmp3) == 0) jumperval += 4;

  SendCmdString(cmstrNoNMEA);  delay(10); Serial.println("Turned off NMEA");
  SendCmdString(cmstrPVTON);  delay(10); Serial.println("Sent PVT ON command");
  SendCmdString(cmstrR50); delay(10); Serial.println("Set 20Hz fix rate");

  if(jumperval & 0x01){
    SendCmdString(cmstrESFON); delay(10); Serial.println("Set ESF ON");
  } else {
    SendCmdString(cmstrESFOFF); delay(10); Serial.println("Set ESF OFF");
  }
  if(jumperval &0x02){ // use ESF-INS if ESF is on
    if(jumperval & 0x01){ // we can use INS messages only if ESF is on
      SendCmdString(cmstrINSON); delay(10); Serial.println("Set INS ON");
      SendCmdString(cmstrMEASOFF); delay(10); Serial.println("Set MEAS OFF");
    } else {  // Have to use ESF-MEAS when ESF is off
      SendCmdString(cmstrINSOFF); delay(10); Serial.println("Set INS OFF");
      SendCmdString(cmstrMEASON); delay(10); Serial.println("Set MEAS ON");
    }
  }  else { // use MEAS
    SendCmdString(cmstrMEASON); delay(10); Serial.println("Set MEAS ON");
    SendCmdString(cmstrINSOFF); delay(10); Serial.println("Set INS OFF");
  }
}



// read the input string and send the output
void SendCmdString(const char *cmstring) {
  uint16_t len = strlen(cmstring);
  uint8_t cbyte, obyte, nybble;
  uint16_t pstate, idx;

  pstate = 0;
  for (idx = 0; idx < len; idx++) {
    cbyte = (uint8_t)cmstring[idx];
    switch(pstate){
      case 0:  
        if(cbyte != 0x20){ // convert first character
          if(cbyte > 0x39){  // adjust 'a' to 'f'
            nybble = 10 + cbyte - (uint8_t)'a';
          } else  nybble = cbyte - '0';
          obyte = nybble << 4;  pstate = 1;
        }       
      break;
      case 1: 
          if(cbyte != 0x20){ // convert first character
            if(cbyte > 0x39){  // adjust 'a' to 'f'
            nybble = 10 + cbyte - (uint8_t)'a';
          } else  nybble = cbyte - '0';
          obyte += nybble;  pstate = 0;
          gpsPort.write(obyte);
          //Serial.printf("%02X\n",obyte);
        }     
      break;
    }
  }
}

With this code, you can use U-Center to generate command strings, which you can add to the code with a bit of editing. U-Center builds the packet and takes care of the checksum generation.
 
Back
Top