microsecond resolution TDMA using GPS PPS on Arduino

A project log for Off-grid GPS (race) tracker client and server

GPS tracker map server and client for use in remote locations with no internet coverage or mains power.

stevesteve 08/22/2016 at 20:350 Comments


GPS Pulse-Per-Second drives hardware interrupt. Tight loop() can give 10 microsecond TDMA time slots whilst still fetching and parsing GPS data. (48MHz Cortex M0)

I need a way of synchronising multiple tracker units so their transmissions do not overlap. The obvious way I can think of is just to assign each device a timeslot - a number of milliseconds past the minute that each device should transmit.

We know some rough figures - we want position updates about once per minute, we want to support > 100 devices per RF channel, and one LoRa 'packet' takes ~ 280ms on-air (See the other project log entry for how these values were calculated).

In its most simple form, we could have 60 devices, and each one transmits on their designated second-past-the-minute. We have a fantastically precise "Pulse Per Second" (PPS) output from the GPS that will synchronise the start of every second across all devices.

But I want more than 60 devices. So I need to come up with a way to accurately time the transmissions to fit in, say, a 1/3 second (333ms) transmit window, to give 3*60=180 possible devices per radio channel (or 90 devices at 30 second refresh rate etc).

Fudging millisecond Time Division Multiple Access in Arduino IDE C

Small real-time-clock modules are available for around £1, which we could add to the Feather, initialise with the time from the GPS, then use a tight loop to reference the time and get the transmission to take place.

I decided against the added hardware complexity and managed it all in software, using the Pulse Per Second output from the GPS in an interrupt routine to trigger the transmission.

In an ideal world, we would have the PPS signal from the GPS trigger a transmission from within the Interrupt Service Routine (ISR). This isn't possible for lots of reasons I could not fix, and isn't advisable as the amount of code, 'delay' functions etc that goes into transmitting a LoRa packet cannot be accomplished inside an ISR (Delay function uses interrupts itself).

The main hurdle I had was accurately triggering a transmission on the sub-second "+333ms" or "second+666ms" timeslots.

I think the most precise way to do this would be for the I/O triggered PPS interrupt to then trigger a timer-based interrupt for the sub-millisecond slot (333/666ms), whose own ISR would then trigger the LoRa transmission, however I couldnt get my head around how to code this up with the Cortex M0 core on the feather.

Instead, I just use the main PPS interrupt to set a flag, then inside the main LOOP() function, keep the function steps small and skippable - so that if a PPS interrupt comes along, our code can skip right to sending the LoRa packet - utilising the perfectly adequate DELAY() function to achieve the inter-second slot timings, since interrupts are once again enabled outside the ISR itself.

Pseudocode below.

Inside the main program loop I perform a number of functions - get serial data from GPS, construct a NMEA sentence, parse into a GPS Object (to store current position, time etc), measure the battery level etc. So long as I can skip over each one, and am not tied up inside each function for any significant time, then we can keep our loop execution duration small and our jitter in potential slot timings low enough.

Is this adequate? For measurement processes I stick a simple digitalWrite(debugpin,HIGH) at the beginning of void loop(), and a matching digitalWrite(debugpin,LOW) at the end, then measure the repetition rate of a loop on a scope when feeding it a constant stream of NMEA data (to represent worst case scenario).

I measure a loop speed of ~128KHz - or a period of 7.8 microseconds, which is a conservative measure of the jitter in the timing of transmission slots when using this method. More than adequate for our purposes! We need to fit a 280ms packet inside a 333ms transmission window, so have many orders of magnitude better precision on our slot timing than is needed. We could easily drop to, say 140ms packet lengths (BW 250 SF 0 CR4/8, implicit), 150ms slots and have 400 devices transmitting once per minute and still have plenty of headroom. ( I suspect our receiver code might then start to struggle!)

This figure is with many additional debug steps and Serial.Print lines added, so the actual jitter will be much less.

Given that all devices will have the same code for the actual LoRa transmission. There will be a constant and consistent delay across all devices when actually triggering the LoRa transmission, so this will not affect our slot synchronisations.

void setup()
//determine our transmission slot based on our device ID
//id1 transmits ON the 00 second of the minute
//id2 transmits at 00 seconds+333ms
//id3 transmits at 00 seconds + 666ms
//id4 transmits at 01 seconds exactly
//lets use ID5:
slotSecond = 01;
slotMillis = 333;
//setup interrupt service routine and attach to a pin. Link io pin to GPS PPS output
void loop()
//for timing the loop on oscilloscope:
//get a byte from serial port, skip if PPS flag is set
if (ppsFlag==FALSE) {
    Serial.Read (1 byte)...etc
//parse a NMEA sentence - skip if PPS flag is set
if (ppsFlag == FALSE and lastserialbyte=0x10)
    //parse GPS Object to update position and current time etc skip if PPS flag is set
   //currentSecond = gps.time.timeseconds()
// read battery level every 10 minutes, skip if PPS flag is set
if (ppsFlag==False and timesincelastbatterylevel>60000)
  vin = AnalogRead(vbat); //etc
//construct LoRa packet ready for transmittion, skip if PPS flag is set
  dataPacket = deviceID+ gps.Latitude + gps.Longitude + vBat...
//finally, if our PPS flag IS set (via the ISR), and the FOLLOWING second matches our timeslot, then transmit
//note that ON the PPS, the NMEA sentence with the newly lapsed second value will not have been received and parsed yet by the main loop, therefore the current second value held in RAM when the PPS is triggered is the PREVIOUS second value. We must therefore add 1 to our current second value to compare against our slot second number. Code below needs correcting for rollover from 59-00
if (ppsFlag==True && currentSecond==(slotSecond-1))
//this is OUR timeslot. We are not in an ISR, so we can now use delay(ms) function to give adequate timing for the sub-second time slots.
//now trigger our transmission
//write debugpin Low for timing
}//end loop

void PPS_ISR()
//our interrupt service routine, triggered once per second by the GPS PPS which will be common across all devices to within <100ns