Forum Rule: Always post complete source code & details to reproduce any issue!
Results 1 to 5 of 5

Thread: UBlox ZED-F9R observations

  1. #1
    Senior Member
    Join Date
    Feb 2018
    Corvallis, OR

    UBlox ZED-F9R observations

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

    // 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:
      // while (!Serial); //Wait for user to open terminal
      Serial.printf("\n\nUBlox Parser Test %s \n", compileTime);
      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;
      if (showDataTimer > 1000) {
          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) {
        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){
            //copyPVTdata(&myPacket); This is where you would save or display the data
        if (myPacket.pktclass == UBX_CLASS_ESF) {
          if (myPacket.pktid == UBX_ESF_MEAS){
            //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 =;
        switch (pstate) {
          case 0:  // check for 0xB5  first preamble byte
            if (pbyte == 0xB5) pstate = 1;
          case 1:  // check for 0x62 second preamble byte
            if (pbyte == 0x62) pstate = 2;
            else pstate = 0; // back to start if not proper preamble
          case 2:  // get pktclass
            initCksum();  // reset checksum
            add2Cksum(pbyte); // add the first byte to checksum
            pptr->pktclass = pbyte; // save packet class
            pstate = 3;
          case 3:  // get pktid
            pptr->pktid = pbyte;
            pstate = 4;
          case 4:  // get len low byte
            pptr->lenlow = pbyte;
            pstate = 5;
            pldsize = pbyte; // save incoming payload length for state 7
          case 5:  // get lenhi
            pptr->lenhi = pbyte;
            pstate = 6;
            pldcount = 0;
            pldsize += ((uint16_t)pbyte << 8);
          case 6:  // get payload until pldcount >= pldsize;
            pptr->payload[pldcount] = pbyte;
            if (pldcount >= pldsize) pstate = 7; // next state when payload full
            if (pldcount > PAYLOADMAX ){ // exit with error before buffer overrun
              pptr->status = STATERROR;
          case 7:  // read first cksum byte
            cksumA = pbyte;
            pstate = 8;
          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
        }  // 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);

  2. #2
    Specs say 14mm accuracy. Do you get anywhere near that?

  3. #3
    Senior Member brtaylor's Avatar
    Join Date
    Mar 2016
    Santa Fe, NM
    Hi you may want to check out my uBlox lib:

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

  4. #4
    Senior Member
    Join Date
    Feb 2018
    Corvallis, OR
    Quote Originally Posted by KrisKasprzak View Post
    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.

  5. #5
    Senior Member
    Join Date
    Feb 2018
    Corvallis, OR
    Quote Originally Posted by brtaylor View Post
    Hi you may want to check out my uBlox lib:

    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:
    // 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];
          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;
          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;
    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.

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts