Clock Synchronization and Time References

Timing and synchronization requirements in LoRaWAN gateways depend on their mode of operation: Indoor gateways which operate purely on LoRaWAN Class A traffic do not require any synchronization mechanism because the only relevant time domain is given by the concentrator’s clock. Nevertheless, LoRa Basics™ Station tracks relative clock drifts and a UTC time reference for convenience, even under this scenario. For outdoor gateways which are equipped with GPS receivers, Station additionally tracks high-precision clock drifts and a time reference to the global GPS time.

The following sections, we look at the synchronization and time reference tracking requirements for each of the clocks involved in a LoRaWAN gateway as well as the time-related strategies implemented in LoRa Basics™ Station.

Clocks and Their Purposes

The gateway operates on various clocks with different purposes:

PPS (pulse per second): The PPS is a 1Hz clock synchronized to global GPS time with a precision in the order of nanoseconds. The main purpose of the PPS in LoRaWAN gateways is to provide a global time reference which allows time-synchronous packet emission of gateways with an accuracy of up to 1 microsecond. This is required for LoRaWAN Class B beacons. The PPS can also be used to calculate the various clock drifts with respect to the global time reference.

SX1301 (xticks): The sx1301 maintains a 32-bit microsecond counter - the xticks counter, which is driven by a 32 MHz temperature-compensated crystal oscillator (TCXO). Receiving packets get timestamped with, and packet transmission times are expressed by, the xticks. If the gateway design allows for access to a PPS, the xticks can be latched to the PPS rising edge. Station abstracts the sx1301’s hardware xticks counter as a monotonic 64-bit microsecond counter: the xtime. All actions-related packet routing (like the allocation of a packet into the TX queue) are expressed in the xtime time domain. LoRaWAN Class B requires a conversion between GPS time and xtime.

Host MCU: Station uses the host MCU’s clock_monotonic to schedule jobs. The most time-critical job is the TX job, which puts a packet into the sx1301’s TX buffer just before the transmission is due. This can be done with an accuracy of a few milliseconds with respect to the sx1301 clock.

Clock Synchronization


The width of pulses generated by crystal oscillators is impacted by various physical conditions, such as ambient temperature, and is subject to product variation. Consequently, any two oscillators will generate pulses of slightly different widths and counters counting those pulses will increment with different speeds. The effect of this could be, for example, that in a given second a hypothetical counter A counted 1000000 (1e6) pulses while hypothetical counter B, driven by a different oscillator, counted 999998 pulses. In this particular example, within one second counter B drifted two microseconds with respect to counter A, which corresponds to 0.0002%, i.e. 2ppm.

Synchronizing a pair of clocks in this context means tracking their relative drift. This allows us to express a given time interval in terms of both clocks. The synchronization precision is a measure of the error we make during conversion of a fixed-time interval between the clocks.

In regular intervals Station executes the ral_getTimesync function, which collects the following timesync measurements based on the RAL implementation:

type timesync_t
sL_t pps_xtime

Last latched sx1301 xticks counter value, accounted for rollovers. 0 if PPS disabled.

sL_t xtime

Instantaneous sx1301 xticks counter value, accounted for rollovers.

ustime_t ustime

MCU system time (clock_monotonic) in microseconds at the moment at which the xticks counter was read (the expected error is in the order of a few milliseconds).

Each measurement round is assigned a timesync quality value which is defined by the time in microseconds it took to fetch the instantaneous xticks counter value. Station keeps statistics on the timesync quality and discards measurements whose quality is considered an outlier. Timesync quality outliers can occur if the Station process is preempted during the SPI transaction where the xticks counter is fetched. This could impact the MCU/SX1301 drift estimation and is therefore discarded.

Based on the timesync measurements the following clock drift statistics are tracked:

PPS <-> SX1301 (if PPS enabled)

The PPS/SX1301 drift characterizes the drift of the SX1301 clock with respect to the global time reference. Compensating for this drift is necessary whenever packets whose transmission time is expressed in GPS time need to be emitted with microsecond-accuracy, i.e. Class B beacons.

This is an example of PPS/SX1301 drift statistics printed into the log:

[SYN:INFO] PPS/SX1301 drift stats: min: -1.3ppm  q50: -1.5ppm  q80: -1.5ppm  max: -1.7ppm - threshold q80: -1.5ppm

MCU <-> SX1301

Station tracks the MCU/SX1301 drift under the assumption that the SX1301 clock is more accurate (i.e. more aligned with the global reference time) than the MCU clock. This is needed to correct time intervals which are expressed in system time for the MCU clock drift. An example where the MCU clock drift correct comes into play is the RefTime field in the updf message which is used by the server to track the IP link latency between the server and the gateway. For downlink packet scheduling, the MCU drift is irrelevant because the scheduling is done purely in the SX1301 clock time domain. Internal job scheduling related to packet transmission does not require MCU drift compensation either. This is because the errors incurred to job scheduling time intervals due to MCU clock drift are in the order of microseconds which is insignificant.

This is an example of MCU/SX1301 drift statistics printed into the log:

[SYN:INFO] MCU/SX1301 drift stats: min: +1.1ppm  q50: +1.6ppm  q80: +1.8ppm  max: +2.8ppm - threshold q90: +2.1ppm

Time Domains and Conversions

References between time domains are tracked as relative offsets between their epochs. In particular, Station keeps track of the UTC time reference with respect to the MCU clock and (if PPS is present) the GPS time reference with respect to the sx1301 clock.

System Time

The system time is the free-running, monotonically-increasing, 64-bit microsecond counter driven by the MCU clock.

ustime_t rt_getTime()

Runtime function to retrieve current system time. Maps to a platform-specific sL_t sys_time() function. On Linux, this calls clock_gettime(CLOCK_MONOTONIC, &t).


Every message from the server down to the gateway can contain the MuxTime field with the UTC timestamp at the moment the message was sent. Therefore, for every server message Station receives, there is an opportunity to adjust the offset between the system time and UTC if the MCU drift is significant. Under this scheme, the Station’s UTC time reference is impacted by various latencies incurred in the message exchange, out of which the network latency is the most severe. This can be tolerated because the UTC time reference is of purely informative value. It is used for timestamping log messages and providing a rough arrival time estimate of uplink packets (rxtime in the upinfo structure).

ustime_t rt_utcOffset

The offset between the system time epoch and UTC epoch in microseconds with a precision of a few hundred milliseconds (network roundtrip latency). Add to rt_getTime() to obtain UTC microsecond timestamp (number of microseconds since UTC epoch). Updated on every server message which contains MuxTime.

ustime_t rt_getUTC()

Convenience function to retrieve current UTC time. Expects rt_utcOffset to be set.

GPS (if PPS enabled)

With PPS enabled, a Station has access to a microsecond counter latched to the last rising edge of the PPS (PPS-latched xticks of the SX1301). In order to convert between xtime (i.e., roll-over compensated xticks) and GPS time, we need to establish how many seconds have passed between an observed PPS pulse and the GPS epoch (00:00h 6-Jan-1980). This is done via a message exchange with the LNS (see Synchronizing PPS to GPS Time). The result of this exchange is a tuple (txtime, gpstime, rxtime) and a value for gpsOffset, which is the difference between the local epoch and the GPS time epoch, i.e., gpsOffset = us_0 - gps_0:

                             gps_s           gpstime
    gps_0  |                    v.... gps_us ....v
     v     |                     ____________________________________
PPS  x-----|____________________|                                         GPS Time
           |   ______________________________________________________
USS     x--|__|... ppsOffset ...                                          Local Time
        ^  |  ^                 ^            ^         ^
     us_0  | us_s            pps_ustime     txtime    rxtime

This example illustrates how the GPS time reference is obtained:

[] Last PPS:       pps_xtime  = 0x520000003906F0
                   pps_ustime = 0xA03F1BEC1D
[] Obtained initial ppsOffset = 561885
[] Timesync message: {'msgtype': 'timesync',
                      'gpstime': 1238942913655858,
                      'txtime': 688254167114}
[] Timesync LNS: tx/rx:0xA03F1C956D..0xA03F1E2975 (103ms432us)
                 us/gps:0xA03F1BEC1D/0x466CFE043F432 (ppsOffset=561885) - 1 solutions
[] Timesync with LNS: gpsOffset=0x466CFE039F240

us_0 - gps_0

(gps_s + gps_0) = (pps_ustime + us_0)

gps_s - pps_ustime = us_0 - gps_0

  1. Fetch last PPS-latched pps_xtime counter and convert it to pps_ustime: pps_ustime = ts_xtime2ustime(pps_xtime)

  2. Calculate ppsOffset = pps_ustime % 1e6

  3. Do a timesync server exchange, which yields txtime/rxtime (local time domain) and gpstime (gps time domain).

  4. Verify that txtime%1e6-ppsOffset < gpstime%1e6 < rxtime%1e6-ppsOffset

ustime_t ppsOffset

The fractional part of the system time second, where the PPS rising edge occurs (in microseconds). This value is refreshed after it drifts more than Q90 of the MCU drift. The value is between 0 and 1e6-1. -1 if no PPS.

sL_t gpsOffset

The offset between the xtime epoch and the GPS time epoch, in microseconds. This value is calculated after a timesync message exchange with the server.

Time Conversion APIs

ustime_t rt_ustime2utc(ustime_t ustime)

Convenience function to convert from system time to UTC by adding rt_utcOffset.

sL_t ts_xticks2xtime(u4_t xticks, sL_t last_xtime)
sL_t ts_xtime2xtime(sL_t xtime, u1_t dst_txunit)
ustime_t ts_xtime2ustime(sL_t xtime)
sL_t ts_ustime2xtime(u1_t txunit, ustime_t ustime)
sL_t ts_xtime2gpstime(sL_t xtime)
sL_t ts_gpstime2xtime(u1_t txunit, sL_t gpstime)

Time Transfer


Why is there no PPS <-> MCU synchronization?