Skip to content
This repository has been archived by the owner on Oct 1, 2022. It is now read-only.

Latest commit

 

History

History
455 lines (336 loc) · 19.3 KB

UBX.md

File metadata and controls

455 lines (336 loc) · 19.3 KB

UBX

This guide describes how the RAWX_Logger_F9P Arduino code works: enabling the ZED-F9P RAWX messages using the binary UBX protocol and logging them to SD card with SdFat

Resources

You can find the latest ZED-F9P documentation on the u-blox website:

UBX Protocol

The UBX binary protocol is defined in section 5 of the interface manual.

UBX message frames comprise:

  • Two sync characters (0xB5 and 0x62)
  • The message class (1 byte)
  • The message ID (1 byte)
  • The message payload length (2 bytes, little endian)
  • The payload
  • A two byte checksum

The SparkFun ublox library does all of the heavy lifting. You don't need to worry about formatting the UBX messages and calculating the checksums etc., the setVal and sendCfgValset functions do all of that for you.

Initialisation: Baud Rate

When power is applied to the ZED-F9P, it starts to output standard NMEA navigation messages on its UART1 port at 38400 Baud (8 data bits, no parity bit, 1 stop bit). The following messages are activated at startup: GGA, GLL, GSA, GSV, RMC, VTG, TXT(INF). See section 8 of the data sheet for further details.

The first thing we need to do is to increase the baud rate to 230400 baud so the interface can cope with the RAWX message rates. The relevant parts of the Arduino code are:

// Include the SparkFun u-blox Library
#include <SparkFun_Ublox_Arduino_Library.h> //http://librarymanager/All#SparkFun_Ublox_GPS
SFE_UBLOX_GPS GPS;

and

// Start the serial port
Serial1.begin(38400);
// Begin the GPS and ignore the response
// (isConnected will return false if the ZED-F9P has already been set to 230400 baud)
GPS.begin(Serial1);
// Change the ZED-F9P UART Baud rate
setUART1BAUD(); // Set ZED-F9P UART1 baud rate to 230400
// Allow time for the Baud rate change
delay(1100);
// Restart serial communications
Serial1.begin(230400); // Restart Serial1 at 230400 baud

The important part is the definition of setUART1BAUD, which uses a UBX-CFG-VALSET message with a key ID of 0x40520001 (CFG-UART1-BAUDRATE) to set the UART1 baud rate in RAM (only) to 230400 baud. See section 5.9.27 of the interface manual for UBX-CFG-VALSET and section 6.7.24 for configuration of the UART1 interface. 230400 in hexadecimal is 0x00038400. The SparkFun ublox library does all of the little-endian conversion for you.

uint8_t setUART1BAUD() {
  return GPS.setVal32(0x40520001, 0x00038400, VAL_LAYER_RAM);
}

Initialisation: UBX messages

In case the logger was reset while the ZED-F9P was sending RAWX messages, we need to make sure the UBX RAWX, SFRBX and TIM_TM2 messages are disabled. We do this by sending a UBX-CFG-VALSET message with key IDs of:

  • 0x209102a5 (CFG-MSGOUT-UBX_RXM_RAWX_UART1)
  • 0x20910232 (CFG-MSGOUT-UBX_RXM_SFRBX_UART1)
  • 0x20910179 (CFG-MSGOUT-UBX_TIM_TM2_UART1)
  • 0x2091002a (CFG-MSGOUT-UBX_NAV_POSLLH_UART1)
  • 0x20910007 (CFG-MSGOUT-UBX_NAV_PVT_UART1)
  • 0x2091001b (CFG-MSGOUT-UBX_NAV_STATUS_UART1)
  • 0x10930006 (CFG-NMEA-HIGHPREC)
  • 0x209100bb (CFG-MSGOUT-NMEA_ID_GGA_UART1)
uint8_t setRAWXoff() {
  GPS.newCfgValset8(0x209102a5, 0x00, VAL_LAYER_RAM);    // CFG-MSGOUT-UBX_RXM_RAWX_UART1
  GPS.addCfgValset8(0x20910232, 0x00);    // CFG-MSGOUT-UBX_RXM_SFRBX_UART1
  GPS.addCfgValset8(0x20910179, 0x00);    // CFG-MSGOUT-UBX_TIM_TM2_UART1
  GPS.addCfgValset8(0x2091002a, 0x00);    // CFG-MSGOUT-UBX_NAV_POSLLH_UART1
  GPS.addCfgValset8(0x20910007, 0x00);    // CFG-MSGOUT-UBX_NAV_PVT_UART1
  GPS.addCfgValset8(0x2091001b, 0x00);    // CFG-MSGOUT-UBX_NAV_STATUS_UART1
  GPS.addCfgValset8(0x20930031, 0x01);    // CFG-NMEA-MAINTALKERID : This line sets the main talker ID to GP
  GPS.addCfgValset8(0x10930006, 0x00);    // CFG-NMEA-HIGHPREC : This line disables NMEA high precision mode
  return GPS.sendCfgValset8(0x209100bb, 0x00);  // CFG-MSGOUT-NMEA_ID_GGA_UART1 : This line disables the GGA message
}

Initialisation: NMEA messages

To avoid NMEA 'noise' when waiting for a GNSS fix, we need to disable the GGA, RMC, GLL, GSA, GSV, VTG, and TXT(INF) messages. We do this by sending a UBX-CFG-VALSET message with key IDs of:

  • 0x209100ca (CFG-MSGOUT-NMEA_ID_GLL_UART1)
  • 0x209100c0 (CFG-MSGOUT-NMEA_ID_GSA_UART1)
  • 0x209100c5 (CFG-MSGOUT-NMEA_ID_GSV_UART1)
  • 0x209100b1 (CFG-MSGOUT-NMEA_ID_VTG_UART1)
  • 0x20920007 (CFG-INFMSG-NMEA_UART1)
  • 0x209100bb (CFG-MSGOUT-NMEA_ID_GGA_UART1)
  • 0x209100ac (CFG-MSGOUT-NMEA_ID_RMC_UART1)

and values of 0:

uint8_t setNMEAoff() {
  GPS.newCfgValset8(0x209100ca, 0x00, VAL_LAYER_RAM);
  GPS.addCfgValset8(0x209100c0, 0x00);
  GPS.addCfgValset8(0x209100c5, 0x00);
  GPS.addCfgValset8(0x209100b1, 0x00);
  GPS.addCfgValset8(0x20920007, 0x00);
  GPS.addCfgValset8(0x209100bb, 0x00);
  return GPS.sendCfgValset8(0x209100ac, 0x00);
}

Initialisation: Talker ID

As the ZED-F9P can track all four major GNSS constellations (GPS, Galileo, GLONASS and BeiDou) concurrently, it will normally output NMEA messages which begin "GN" instead of "GP". This could confuse some NMEA parsing code so we need to change the "talker ID" to "GP". We do this by sending a UBX-CFG-VALSET message with a key ID of:

  • 0x20930031 (CFG-NMEA-MAINTALKERID)

and a value of 1:

uint8_t setTALKERID() {
  return GPS.setVal8(0x20930031, 0x01, VAL_LAYER_RAM);
}

Initialisation: Set Measurement Rate

During RAWX logging, the measurement rate will be increased to (e.g.) 4 Hz. During initialisation, we need to make sure it is set back to 1 Hz. We do this by sending a UBX-CFG-VALSET message with a key ID of:

  • 0x30210001 (CFG-RATE-MEAS)

and a value of 1000 milliseconds. 1000 in hexadecimal is 0x03e8:

uint8_t setRATE_1Hz() { return GPS.setVal16(0x30210001, 0x03e8, VAL_LAYER_RAM); }

Later we will use a value of 250 milliseconds (0x00fa) to set the rate to 4 Hz for RAWX logging:

uint8_t setRATE_4Hz() { return GPS.setVal16(0x30210001, 0x00fa, VAL_LAYER_RAM); }

Initialisation: Set Navigation Dynamic Model

For the base logger, we need to set the navigation dynamic model to STATionary. We do this by sending a UBX-CFG-VALSET message with a key ID of:

  • 0x20110021 (CFG-NAVSPG-DYNMODEL)

and a value of 2:

uint8_t setNAVstationary() { return GPS.setVal8(0x20110021, 0x02, VAL_LAYER_RAM); };

For the rover logger, we set the dynamic model to "AIR1" (airborne with <1g acceleration) using a value of 6:

uint8_t setNAVair1g() { return GPS.setVal8(0x20110021, 0x06, VAL_LAYER_RAM); };

The values for the other dynamic models are defined at the end of section 6.7.12 in the interface manual.

Set the RTC

GPS.getFixType() will return a non-zero value once the ZED-F9P has established a fix. We can then use the GNSS (UTC) time to set the SAMD Real Time Clock:

// Set and start the RTC
alarmFlag = false; // Make sure alarm flag is clear
rtc.begin(); // Start the RTC
rtc.setTime(GPS.getHour(), GPS.getMinute(), GPS.getSecond()); // Set the time
rtc.setDate(GPS.getDay(), GPS.getMonth(), (uint8_t)(GPS.getYear() - 2000)); // Set the date

We can then use RTC alarm interrupts to close the RAWX log file and open a new one every INTERVAL minutes:

rtc.setAlarmSeconds(0); // Set RTC Alarm Seconds to zero
uint8_t nextAlarmMin = ((GPS.getMinute()+INTERVAL)/INTERVAL)*INTERVAL; // Calculate next alarm minutes
nextAlarmMin = nextAlarmMin % 60; // Correct hour rollover
rtc.setAlarmMinutes(nextAlarmMin); // Set RTC Alarm Minutes
rtc.enableAlarm(rtc.MATCH_MMSS); // Alarm Match on minutes and seconds
rtc.attachInterrupt(alarmMatch); // Attach alarm interrupt

We can also use the RTC to set the create, write and access timestamps of the log file using SdFat.

RAWX messages

Now that the ZED-F9P has established a fix and we have set the RTC, we now need to speed up the measurement rate to 4 Hz using the setRATE_4Hz message we defined earlier.

Finally, we can enable the RAWX, SFRBX, TIM_TM2 and any other messages we want to log. We do this by sending a UBX-CFG-VALSET message with key IDs of:

  • 0x209102a5 (CFG-MSGOUT-UBX_RXM_RAWX_UART1)
  • 0x20910232 (CFG-MSGOUT-UBX_RXM_SFRBX_UART1)
  • 0x20910179 (CFG-MSGOUT-UBX_TIM_TM2_UART1)
  • 0x2091002a (CFG-MSGOUT-UBX_NAV_POSLLH_UART1)
  • 0x20910007 (CFG-MSGOUT-UBX_NAV_PVT_UART1)
  • 0x2091001b (CFG-MSGOUT-UBX_NAV_STATUS_UART1)
  • 0x10930006 (CFG-NMEA-HIGHPREC)
  • 0x209100bb (CFG-MSGOUT-NMEA_ID_GGA_UART1)

and values (rates) of 1:

uint8_t setRAWXon_noWait() {
  GPS.newCfgValset8(0x209102a5, 0x01, VAL_LAYER_RAM);
  GPS.addCfgValset8(0x20910232, 0x01);
  GPS.addCfgValset8(0x20910179, 0x01);
  GPS.addCfgValset8(0x2091002a, 0x00);   // Change the last byte from 0x00 to 0x01 to enable NAV_POSLLH
  GPS.addCfgValset8(0x20910007, 0x01);   // Change the last byte from 0x01 to 0x00 to leave NAV_PVT disabled
  GPS.addCfgValset8(0x2091001b, 0x01);   // This line enables the NAV_STATUS message
  GPS.addCfgValset8(0x20930031, 0x03);   // This line sets the main talker ID to GN
  GPS.addCfgValset8(0x10930006, 0x01);   // This sets the NMEA high precision mode
  return GPS.sendCfgValset8(0x209100bb, 0x01, 0); // This (re)enables the GGA mesage
}

If you study the code, you will see that the NAV_POSLLH, NAV_PVT and NAV_STATUS are also enabled and logged. You can disable these by editing the setRAWXon message definition. However, I would recommend leaving NAV_STATUS enabled as the code uses this to indicate when Survey_In mode has achieved a TIME solution.

The latest version of the logger code can log NMEA format messages to the SD card too. This is useful as RTKLIB can extract high precision GNGGA messages from the log file. Have a look at the definition for setRAWXon in the code, you will be able to see the key IDs that enable the GNGGA message and high precision mode. setRAWXoff disables the high precision mode in case the extra decimal places in the latitude and longitude confuse the SparkFun ublox library.

Opening the log file

We can now be confident that the UBX RXM_RAWX, RXM_SFRBX and TIM_TM2 messages are being produced. So all we need to do is open a log file on the SD card and throw everything we receive on Serial1 into it. You will find the code that opens the log file starting with the line:

case open_file:

Write the RAWX messages to the log file

You will find the code that writes the RAWX messages to the log file starting with the line:

case write_file:

To make the writing as efficient and as fast as possible, data is written to the SD card by SdFat in packets of 512 bytes (SDpacket).

Restart Logging, Stop Button and Low Battery

The code checks the UBX serial data continuously, counting the number of bytes and calculating the expected checksum for each message. If the checksum does not match, due to an error or dropped byte in the serial data, the log file is automatically closed and a new one opened. You can find the code that does this starting with the line:

case restart_file:

The code also checks to see if the stop button has been pressed or if the battery is low. If either condition is true, the code will close the log file.

If the stop button was pressed, the code will do nothing more until the Adalogger is reset.

If the battery voltage became too low, the code will wait until the voltage recovers (e.g. when a new UAV battery is inserted) and then it will start logging again.

Open a new log file

After an RTC alarm interrupt, the code will close the current log file and open a new one. You will find the code that does that starting with the line:

case new_file:

You can change how often a new file is opened by changing the value of INTERVAL in the code before you upload it to the Adalogger.

The code will wait until it gets to the end of the current UBX frame before opening the new file, to ensure the data in the individual log files is as clean and contiguous as possible.

Serial UBX messages sent from the Adalogger to the ZED-F9P are all acknowledged (or nacknowledged) by a short ack/nack message. The code expects these and discards them without writing them into the log file. RTKLIB can probably cope with these acks/nacks being in the log files, but the code does try to make life as easy as possible for RTKLIB by discarding them.

Checking everything is OK

The length of the RAWX messages increases depending on how many satellites are being tracked. We need to check that we are not trying to log more data than the serial interface and SD card can cope with.

If you have access to an oscilloscope, you can check how much data is being received on the Adalogger RX pin. You should see bursts of data every 250 milliseconds. The RX line goes high (3.3V) when idle. Check that the RX line is not continuously busy. There must be gaps at the end of each 250 millisecond interval. If the gaps are small, you may need to increase the UART baud rate higher than 230400 baud or decrease the RAWX measurement rate to 2 Hz or lower.

Serial.JPG

Likewise, use your oscilloscope to monitor the red LED (digital pin 13). The red LED is on during SD card writes. Again the code must not be writing to the card continuously. There must be gaps between writes every 250 milliseconds.

SDwrite.JPG

If the SD card is continously busy: replace your SD card with a faster one; decrease the RAWX measurement rate; or consider changing the SdFat clock speed by editing the line which says:

if (!sd.begin(cardSelect, SD_SCK_MHZ(50))) {

Serial RX Buffer

The RAWX data rates can be high when the logger is tracking multiple satellites. This could be a problem when closing one SD log file and opening the next as we need to rely on the serial receive buffer being large enough to buffer the data until the new file is open. Unfortunately, by default, the SERIAL_BUFFER_SIZE is only 256 bytes, which isn't large enough and causes data to be dropped.

In previous projects, I have recommended increasing the size of the serial buffer by editing the file RingBuffer.h and changing the value of SERIAL_BUFFER_SIZE. This isn't an efficient way to increase the buffer size as:

  • both receive and transmit buffers for both Serial1 and Serial5 are increased in size, so you end up using four times as much RAM as necessary
  • the buffer size will be reset each time the Adafruit boards is updated

In this project, we work around this by creating a separate large SerialBuffer using the same class as a normal serial RingBuffer. A timer interrupt is used to check for the arrival of Serial1 data and move it into SerialBuffer. The main loop then reads the data from SerialBuffer using the inherited .available and .read_char functions.

Here is the line that defines the large SerialBuffer:

// Define SerialBuffer as a large RingBuffer which we will use to store the Serial1 receive data
// That way, we do not need to increase the size of the Serial1 receive buffer (by editing RingBuffer.h)
RingBufferN<16384> SerialBuffer; // Define SerialBuffer as a RingBuffer of size 16384 bytes

Here is the code that sets up the TC3 timer interrupt. It is based on the code provided by Sheng Chen jdneo

// TimerCounter3 functions to copy Serial1 receive data into SerialBuffer
#define CPU_HZ 48000000
#define TIMER_PRESCALER_DIV 16

// Set TC3 Interval (sec)
void setTimerInterval(float intervalS) {
  int compareValue = intervalS * CPU_HZ / TIMER_PRESCALER_DIV;
  if (compareValue > 65535) compareValue = 65535;
  TcCount16* TC = (TcCount16*) TC3;
  // Make sure the count is in a proportional position to where it was
  // to prevent any jitter or disconnect when changing the compare value.
  TC->COUNT.reg = map(TC->COUNT.reg, 0, TC->CC[0].reg, 0, compareValue);
  TC->CC[0].reg = compareValue;
  while (TC->STATUS.bit.SYNCBUSY == 1);
}

// Start TC3 with a specified interval
void startTimerInterval(float intervalS) {
  REG_GCLK_CLKCTRL = (uint16_t) (GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 | GCLK_CLKCTRL_ID_TCC2_TC3) ;
  while ( GCLK->STATUS.bit.SYNCBUSY == 1 ); // wait for sync

  TcCount16* TC = (TcCount16*) TC3;

  TC->CTRLA.reg &= ~TC_CTRLA_ENABLE;
  while (TC->STATUS.bit.SYNCBUSY == 1); // wait for sync

  // Use the 16-bit timer
  TC->CTRLA.reg |= TC_CTRLA_MODE_COUNT16;
  while (TC->STATUS.bit.SYNCBUSY == 1); // wait for sync

  // Use match mode so that the timer counter resets when the count matches the compare register
  TC->CTRLA.reg |= TC_CTRLA_WAVEGEN_MFRQ;
  while (TC->STATUS.bit.SYNCBUSY == 1); // wait for sync

  // Set prescaler to 16
  TC->CTRLA.reg |= TC_CTRLA_PRESCALER_DIV16;
  while (TC->STATUS.bit.SYNCBUSY == 1); // wait for sync

  setTimerInterval(intervalS);

  // Enable the compare interrupt
  TC->INTENSET.reg = 0;
  TC->INTENSET.bit.MC0 = 1;

  NVIC_SetPriority(TC3_IRQn, 3); // Set the TC3 interrupt priority to 3 (lowest)
  NVIC_EnableIRQ(TC3_IRQn);

  TC->CTRLA.reg |= TC_CTRLA_ENABLE;
  while (TC->STATUS.bit.SYNCBUSY == 1); // wait for sync
}

And here is the interrupt handler that copies the serial data from the (small) serial receive buffer into our (large) RingBuffer:

// TC3 Interrupt Handler
void TC3_Handler() {
  TcCount16* TC = (TcCount16*) TC3;
  // If this interrupt is due to the compare register matching the timer count
  // copy any available Serial1 data into SerialBuffer
  if (TC->INTFLAG.bit.MC0 == 1) {
    TC->INTFLAG.bit.MC0 = 1;
    int available1 = Serial1.available(); // Check if there is any data waiting in the Serial1 RX buffer
    while (available1 > 0) { 
        SerialBuffer.store_char(Serial1.read()); // If there is, copy it into our RingBuffer
        available1--;
    }
  }
}

In the main loop, we enable the timer interrupt after setting the RTC:

// Now that Serial1 should be idle and the buffer empty, start TC3 interrupts to copy all new data into SerialBuffer
// Set the timer interval to 10 * 10 / 230400 = 0.000434 secs (10 bytes * 10 bits (1 start, 8 data, 1 stop) at 230400 baud)
startTimerInterval(0.000434); 

Then to read data from SerialBuffer, we can use the inherited .available and .read_char methods:

if (SerialBuffer.available()) {
  uint8_t c = SerialBuffer.read_char();

The interrupt service routine takes between 3 and 25 usec to execute depending on how many characters are available (0 to 10). This is a significant overhead given that the ISR runs every 434 usec, but it is a price worth paying to avoid having to edit RingBuffer.h.

TC3_ISR_1.JPG

TC3_ISR_2.JPG

RAWX_Logger_F9P_I2C

RAWX_Logger_F9P_I2C code uses the I2C port to do all of the message configuration, instead of UART. This makes the code much more efficient as the UART port can be dedicated exclusively to the RAWX messages; all data received over UART can be simply streamed onto the SD card without having to worry about having to extract UBX message acknowledgements. Please give it a try if you want to but remember that you will need to connect the SDA and SCL pins on the Adalogger to the GPS-RTK2 board.

Enjoy!

Paul