Time synchronization over the air

In a network of multiple devices, it is often required that all participants operate on a common time base. This document describes how to synchronize multiple CC13x0 devices using the proprietary PHY.

../_images/aafig-09150de118801c7b42d331d3c51aef36700a84e3.png

In our example we have a master and a slave and we want to synchronize the slave clock to the master’s clock. Both clocks run at the same tick rate, but the slave clock has an initial offset T relative to the master. Assuming that m_n and s_n specify absolute timestamps with index n on the master and the slave, respectively, we get the relationship:

(1)s_n = m_n + T

Initially, the slave doesn’t know T and hence, s_n is always different from the master’s time m_n.

As the timer domain, we choose the radio timer RAT on the RF core which provides 32 bit timestamps at 4 MHz (4 ticks per µs). RAT timestamps can be used as absolute start and end triggers for radio operation commands.

One-way synchronization example

In order to get T, it would be enough to send one message at a timestamp m_0 from the master to the slave as shown in the sequence chart below:

../_images/time-synchronization-oneway.png

Figure 31. Sequence chart of the one-way time synchronization process.

The slave would receive the message after a certain delay d_{\text{TX}} at s_1 and could deduce T by the following formula:

(2)T = m_0 - \left( s_1 - d_{\text{TX}} \right)

s_1 is the time of the receiver signal Syncword found. The transmission offset d_{\text{TX}} contains the time to bring up the RF front-end, to calibrate the synthesizer and to send the packet preamble and the syncword. The time of flight (TOF) is very small compared to that value and can be ignored. d_{\text{TX}} is a fixed value and depends on the chosen RF settings. It must be measured/only once during development and is then compiled into the application.

A two-way synchronization algorithm

This section describes an indirect approach that doesn’t require to measure d_{\text{TX}}. But it can also be used to deduce d_{\text{TX}} indirectly. We use an algorithm similar to the Network Time Protocol.

The following sequence diagram shows the synchronization process.

../_images/time-synchronization-twoway.png

Figure 32. Sequence chart of the two-way time synchronization process.

The slave sends a synchronization request to the master at s_0 and sets s_0 as an absolute start trigger for the TX command.

The master receives the message after d_{\text{TX}} at m_1. This is the timestamp that is appended as meta data to the packet and specifies the time when the sync word was found:

(3)m_1 = s_0 + d_{\text{TX}}

After a short time d_{\text{Processing}}, the master responds to the request with a reply message at m_2. It sets the TX command start trigger to m_2 and includes m_1 into the packet payload as well:

(4)m_2 = m_1 + d_{\text{Processing}}

When the client receives the reply at s_3, it has the following timing information and can calculate the initial clock offset T:

  • s_0: Request sent by the client
  • m_1: Request received by the master
  • s_3: Reply received by the client
  • d_{\text{Processing}}: A fixed delay for message processing on the master

From (1), we know that:

(5)T = s_0 - m_0

We do not know m_0, but we know its offset to m_1 which is d_{\text{TX}}:

(6)m_0 = m_1 - d_{\text{TX}}

With (5) and (6) we get:

(7)T = s_0 - (m_1 - d_{\text{TX}})

We still do not know d_{\text{TX}}, but we know that it is included into the round-trip time s_3 - s_0 as follows:

(8)s_3 - s_0 = 2 \cdot d_{\text{TX}} + d_{\text{Processing}}

With the help of (8) we can rewrite (7) to:

(9)T = s_0 - \left( m_1 - \frac{s_3 - s_0 - d_{\text{Processing}}}{2} \right)

and finally obtain T. This value must now be added to any further RF operation on the client.

Sending and receiving timestamp messages

This section provides code snippets for implementing the above algorithms. When sending a timestamp message at a certain time, we use txTimestamp as an absolute start trigger, but include it also into the packet payload:

// Convenience macro
#define RF_convertMsToRatTicks(milliseconds) \
    (((uint32_t)(milliseconds)) * 1000 * 4)

// Exported from SmartRF Studio
rfc_CMD_PROP_TX_t txCommand;

// Set a time in the near future (5ms)
uint32_t txTimestamp = RF_getCurrentTime() + RF_convertMsToRatTicks(5);

// Set txTimestamp as an absolute start trigger
txCommand.startTrigger.triggerType = TRIG_ABSTIME;
txCommand.startTime = txTimestamp;

// Include it also into the payload
uint32_t payload[1] = { txTimestamp };
txCommand.pPkt = (uint8_t*)&payload[0];
txCommand.pktLen = sizeof(payload);

When receiving this packet, the receiver must read the timestamp from the packet payload, but must also configure the RF core to append a timestamp to each received packet. This is the time when the RF core raises the signal “Synchronization found” and we choose the name rxTimestamp:

// Exported from SmartRF Studio
rfc_CMD_PROP_RX_t rxCommand;

// Append RX timestamp to the payload
RF_cmdPropRx.rxConf.bAppendTimestamp = 1;

// The code to execute the RX command and to setup
// the RX data queue is not shown here.

// When reading the packet content from the
// RX data queue, rxTimestamp is behind the payload:
rfc_dataEntryGeneral_t* currentDataEntry = RFQueue_getDataEntry();
// Assuming variable length
uint8_t packetLength = *(uint8_t*)(&currentDataEntry->data);
uint8_t* packetDataPointer = (uint8_t*)(&currentDataEntry->data + 1);
uint32_t rxTimestamp;
memcpy(&rxTimestamp, &packetDataPointer + packetLength, 4);

// The TX timestamp is found in the payload
uint32_t txTimestamp;
memcpy(&rxTimestamp, packetDataPointer + packetLength, 4);