Localization Toolbox

The Localization Toolbox, or RTLS Toolbox, is a collection of RTLS (Real Time Locating System) techniques that can be implemented on TI’s standard Bluetooth low energy radios in the CC26xx series. These techniques provide raw data that can be used for locating, tracking, and securely range bounding other Bluetooth nodes.

The inherent flexibility of the CC26xx RF Core is what enables this significant extension of function beyond merely communications, and the main advantages are that customers can start adding RTLS features and security with little or no extra cost, very little additional energy consumption and no increase in peak power.

There are two fundamentally different approaches to location finding:

Trilateration Triangulation
../_images/trilateration.png
../_images/triangulation.png

Trilateration is where you know the distance between a reference node and a target node. This means that the possible locations seen by one locator constitute a circle, so typically three locators are needed to find a single common intersect point. (Assuming a 2D scenario)

Time of Flight gives you the distance from the receiver to the transmitter.

Triangulation is where you know the direction from a reference node to a target node. This means that the possible locations seen by one locator constitute a straight line, so two nodes will be enough to determine a single intersect point. (Assuming a 2D scenario)

Angle of Arrival gives you the angle from the receiver to the transmitter.

Angle of Arrival

Angle-of-Arrival (AoA) is a technique for finding the direction that an incoming Bluetooth packet is coming from, creating a basis for triangulation.

An array of antennas with well-defined properties is used, and the receiver will switch quickly between the individual antennas while measuring the phase shift resulting from the small differences in path length to the different antenna.

These path length differences will depend on the direction of the incoming RF waves relative to the antennas in the array. In order to facilitate the phase measurement, the packet must contain a section of continuous tone (CT) where there are no phase shifts caused by modulation.

Packet Format

In order to get a good estimate of ϕ (phase), all other intentional phase shifts in the signal should be removed. AoA packets (based on the BLE PHY) achieve this by adding a section of consecutive 1’s as part of the PDU, effectively transmitting a single tone at the carrier frequency + 250kHz.

../_images/aoa_packet.png

This gives the receiver time to synchronize the demodulator first, and then store I and Q samples from the single tone 250kHz section at the end into a buffer and the buffer can then be post-processed by an AoA application

In TX, the RF Core patch ensures that the tone is inserted in the PDU without being distorted by the whitening filter and without invalidating the CRC.

In RX, the RF Core patch analyzes the packet and starts capturing samples at the right time while synchronizing antenna switching. The samples are left in the RF Core RAM for analysis by the main MCU

Note

The I/Q Data Sample is the coordinates of your signal as seen down the time axis. In fact, I/Q data is merely a translation of amplitude and phase data from a polar coordinate system to a Cartesian (X,Y) coordinate system and using trigonometry, you can convert the polar coordinate sine wave information into Cartesian I/Q sine wave data.
../_images/iq_phasor_diagram.png

Integration

Using a special RF Core patch in receive mode, the I and Q samples from the transmitted carrier frequency + 250kHz tone can be captured, pre-processed, and buffered by the RF Core without any load on the main MCU.

Due to the pre-processing, the application can determine the phase shift without having to remove DC offset or IF first, significantly simplifying the estimation process and leaving the application MCU free to do more on top.

../_images/processing_phase.png

The I/Q samples can be captured at a rate of up to 4 MHz and a resolution of 16 bits (32 bits per I/Q data pair) -> 128 bits/μs -> 1kB holds 64 μs and the buffer size can be up to 2kB (128us of 4MS/s data)

The RF Core can provide event which can be mapped to DMA to trigger the start of GPTimer. The GPTimer is configured as 4us count down mode, once it reaches 4us, then it will generate another DMA transaction to toggle antenna switches.

AoA Driver

The AoA driver is responsible for antenna switching, data extraction and event routing.

Data Collection Flow

Setup radio timer (RAT) channel to do capture when a AoA packet is received, All the code snippet in this section is included in AoA.c

Listing 78. The RAT current count is captured in RATCH7VAL
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// CMD_SET_RAT_CPT
// Set up RAT capture: RAT Channel 7, Rising edge, Single capture, and InputSrc 11
rfc_CMD_SET_RAT_CPT_t RF_cmdSetRatCpt =
{
    .commandNo = CMD_SET_RAT_CPT,
    .config.inputSrc = 11,
    .config.ratCh = 7,
    .config.bRepeated = 0,
    .config.inputMode = 0,
};
Listing 79. When RATCH7VAL compare matches, the radio patch will trigger the RAT_GPO4 event.
1
2
3
4
5
6
7
8
9
// CMD_SET_RAT_OUTPUT
// Setup IO configuration for RAT_GPO4, Pulse mode, RAT Channel 7
rfc_CMD_SET_RAT_OUTPUT_t RF_cmdSetRatOutput =
{
    .commandNo = CMD_SET_RAT_OUTPUT,
    .config.outputSel = 4,
    .config.outputMode = 0,
    .config.ratCh = 7,
};
Listing 80. Route RAT_GPO4 event to DMACH14
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
///// 1. RFC_IN_EV4 route to DMACH14                 ///////
HWREG(EVENT_BASE + EVENT_O_UDMACH14BSEL) = EVENT_UDMACH14BSEL_EV_RFC_IN_EV4;

///// 2. Set up DMACH14, 1 to GPTn:CTL:TnEN register ///////
if (NULL == udmaHandle) udmaHandle = UDMACC26XX_open();

uDMAChannelControlSet(UDMA0_BASE, UDMA_CHAN_DMA_PROG | UDMA_PRI_SELECT, //ch #14
                      UDMA_SIZE_32 | UDMA_SRC_INC_NONE |
                      UDMA_DST_INC_NONE |
                      UDMA_ARB_1);
Listing 81. Use DMACH14 to trigger Timer 0A start
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
static void patternSetupDmaTransfer(AoA_Pattern *pattern)
{
    //...
    static volatile uint32_t timerEnableWord = 1;
    uDMAChannelTransferSet(UDMA0_BASE, UDMA_CHAN_DMA_PROG | UDMA_PRI_SELECT, //ch #14
                           UDMA_MODE_BASIC, //single transfer
                           (void *)&timerEnableWord,//source address
                           //(void *)&HWREGBITW(GPT0_BASE + GPT_O_CTL, GPT_CTL_TAEN_BITN), //destination address
                           (void *)(GPT0_BASE + GPT_O_CTL),
                           1);
    uDMAChannelEnable(UDMA0_BASE, UDMA_CHAN_DMA_PROG);
    //...
}

GPTimer 0A is configure as periodic up with 4us timeout and the timeout(timer match) event is routed to DMACH19 to trigger antenna switching.

Listing 82. GPTimer 0A and DMACH9 configuration
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
///// 3. Enable DMA trigger on GPT compare match ///////
//HWREGBITW(GPT0_BASE + GPT_O_DMAEV, GPT_DMAEV_TAMDMAEN_BITN) = 1;
HWREGBITW(GPT0_BASE + GPT_O_DMAEV, GPT_DMAEV_TATODMAEN_BITN) = 1;

///// 4. Connect GPT0 DMA req to Channel 9 / DMA_CHAN_TIMER0_A ///////
//HWREG(EVENT_BASE + EVENT_O_UDMACH9BSEL) = EVENT_UDMACH9BSEL_EV_GPT0A_DMABREQ;
HWREG(EVENT_BASE + EVENT_O_UDMACH9SSEL) = EVENT_UDMACH9SSEL_EV_GPT0A_DMABREQ;

///// 5. Copy a toggle entry into GPIOTGL on timer match ///////
uDMAChannelControlSet(UDMA0_BASE, UDMA_CHAN_TIMER0_A | UDMA_PRI_SELECT, //ch #9
                      UDMA_SIZE_32 | UDMA_SRC_INC_32 |
                      UDMA_DST_INC_NONE |
                      UDMA_ARB_1);

When there is AoA packet received (AOA.c), DMA will transfer patterns to toggle the antennas on the BOOSTXL-AoA board. The g_initialPeriod is delay for GPTimer. The reason for adding the delay is that when radio core notifies the application that an AoA packet is received, the actual tone has not arrived yet. Therefore, there is delay added to the timer start to ensure that the antenna only toggles when the tone arrives.

The delay number was found in lab measurement. However, this number should not be changed unless you want to start your antenna toggling later.

Listing 83. Route DMACH9 to GPIO toggle register and set up initial timer delay
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
uint32_t g_initialPeriod = 1185;    // 1385

static void patternSetupDmaTransfer(AoA_Pattern *pattern)
{
    uDMAChannelTransferSet(UDMA0_BASE, UDMA_CHAN_TIMER0_A | UDMA_PRI_SELECT, //ch #9
                           UDMA_MODE_BASIC, //single transfer
                           (void *)pattern->toggles,//source address
                           (void *)(GPIO_BASE + GPIO_O_DOUTTGL31_0), //destination address
                           pattern->numPatterns);
    uDMAChannelEnable(UDMA0_BASE, UDMA_CHAN_TIMER0_A);

    // Re-enable timer start
    static volatile uint32_t timerEnableWord = 1;
    uDMAChannelTransferSet(UDMA0_BASE, UDMA_CHAN_DMA_PROG | UDMA_PRI_SELECT, //ch #14
                           UDMA_MODE_BASIC, //single transfer
                           (void *)&timerEnableWord,//source address
                           //(void *)&HWREGBITW(GPT0_BASE + GPT_O_CTL, GPT_CTL_TAEN_BITN), //destination address
                           (void *)(GPT0_BASE + GPT_O_CTL),
                           1);
    uDMAChannelEnable(UDMA0_BASE, UDMA_CHAN_DMA_PROG);


    // First period is 4µs + 4µs * 6 - 2µs
    HWREG(GPT0_BASE + GPT_O_TAV) = g_initialPeriod;
}

After antenna toggling, the IQ samples from the received packet are extracted by the following function.

Listing 84. Extract I/Q sample from radio RAM
1
void AOA_getRxIQ(uint8_t *packetId, AoA_IQSample **samples)

Then the data get passed into the AOA_getPairAngles() to get the estimated angle between receiver and sender.

Listing 85. Angles estimation
1
2
3
4
void AOA_getPairAngles(const uint8_t channel,
                       const AoA_AntennaConfig *antConfig,
                             AoA_AntennaResult *antResult,
                             AoA_IQSample *sample)

Antenna Switching

Control pattern is placed in ant_array1_config_boostxl_rev1v1.c and ant_array2_config_boostxl_rev1v1.c: We will focus on code for antenna array 1. The same theory applies for antenna array 2 as well.

The ‘AOA_PIN()’ is just a bitwise operation. If you want to use your own design(different IOs), you will need to change the parameter passed on to AOA_PIN function.

Listing 86. Choosing IOs for antenna control
1
2
3
4
5
6
7
// User defined nice-names for the pins
#define AOA_A1_SEL     AOA_PIN(IOID_27)
#define AOA_Ax_ANT1    AOA_PIN(IOID_28)
#define AOA_Ax_ANT2    AOA_PIN(IOID_29)
#define AOA_Ax_ANT3    AOA_PIN(IOID_30)

#define AOA_PIN(x) (1 << (x&0xff))

The initial pattern suggested that in our example second antenna under antenna array group 1 is used to receive the incoming packets at the beginning and the end.

Listing 87. Antenna patten setup
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
AoA_Pattern antennaPattern_A1 = {
    .numPatterns = 32,
    .initialPattern = AOA_A1_SEL | AOA_Ax_ANT2,
    .toggles =
    {
     AOA_A1_SEL | AOA_Ax_ANT1, // A1.1
     AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
     AOA_A1_SEL | AOA_Ax_ANT3, // A1.3

     AOA_A1_SEL | AOA_Ax_ANT1, // A1.1
     AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
     AOA_A1_SEL | AOA_Ax_ANT3, // A1.3

     AOA_A1_SEL | AOA_Ax_ANT1, // A1.1
     AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
     AOA_A1_SEL | AOA_Ax_ANT3, // A1.3

     AOA_A1_SEL | AOA_Ax_ANT1, // A1.1
     AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
     AOA_A1_SEL | AOA_Ax_ANT3, // A1.3

     AOA_A1_SEL | AOA_Ax_ANT1, // A1.1
     AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
     AOA_A1_SEL | AOA_Ax_ANT3, // A1.3

     AOA_A1_SEL | AOA_Ax_ANT1, // A1.1
     AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
     AOA_A1_SEL | AOA_Ax_ANT3, // A1.3

     AOA_A1_SEL | AOA_Ax_ANT1, // A1.1
     AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
     AOA_A1_SEL | AOA_Ax_ANT3, // A1.3

     AOA_A1_SEL | AOA_Ax_ANT1, // A1.1
     AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
     AOA_A1_SEL | AOA_Ax_ANT3, // A1.3

     AOA_A1_SEL | AOA_Ax_ANT1, // A1.1
     AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
     AOA_A1_SEL | AOA_Ax_ANT3, // A1.3

     AOA_A1_SEL | AOA_Ax_ANT1, // A1.1
     AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
     AOA_A1_SEL | AOA_Ax_ANT3, // A1.3

     AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
     AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
    }
};

After that, once AoA packet is detected, we will start toggling IOs to choose different antenna in order to gather the phase difference between each antenna. Here we use GPIO:DOUTTGL31_0 register to enable the toggling function. For more information, please see Driverlib Documentation CC2640R2 -> CPU domain register descriptions -> GPIO:DOUTTGL31_0

The first antenna used is second antenna which is controlled by IOID_29 and then we want to change to a different antenna(assuming antenna 1 IOID_28). This means that we need to toggle both IOID_29 and IOID_28. Then we need to set both bit[29] and bit[28] to 1 in register GPIO:DOUTTGL31_0 in order to toggle.

In our software solution, the following function is taking care of toggling pattern generation. It takes the previous selected IOID_x and XOR with the current selected IOID_x.

Listing 88. Function to generate IO toggle patten
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void AOA_toggleMaker(const uint32_t *in, uint32_t initState, uint32_t len, uint32_t *out)
{
    uint32_t currState = initState;

    for(int i=0; i<len; ++i)
    {
        uint32_t tgl = currState ^ in[i];
        currState ^= tgl;
        out[i] = tgl;
    }
}

Therefore all you need is to design the pattern and then call the following two functions to initialize the patterns (assuming you have two arrays). These two functions are called in aoa_receiver.c static void AoAReceiver_init(void).

Listing 89. Function to generate IO toggle patten
1
2
BOOSTXL_AoA_AntennaPattern_A1_init();
BOOSTXL_AoA_AntennaPattern_A2_init();

Convert I/Q Data to Angle Difference

When the radio frequency wave incident on to an antenna array(assuming there are only 2 antennas on the board) and arrives at different antennas at different time, there will be phase difference between the antennas. So we extract the phase difference between ant1_sample[0 to 7] and ant2_sample[0 to 7]. The switch among antennas will cause measurement error, therefore we discard I/Q samples from 8 to 15 when calculating angles.

The I/Q data can be presented into a X-Y domain with real number I and imaginary number Q (90 degree difference). As mentioned before, for each period of 250 kHz signal, we sample 16 I and Q data. If there is no difference, that means that the I/Q data is the same, therefore the phase between ant_1 sample1 will be the same to ant_2 sample1.

../_images/ant1_ant2_samplen.png
../_images/anlge_of_interest.png

Here is the code putting I/Q data into 2 dimension and then calculate the angle difference using arctan(Za*complexconjugate(Zb)).

Listing 90. Math for angle calculation.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Calculate phase difference between antenna a vs. antenna b
int16_t Pab_rel = AngleComplexProductComp(sample[r*numAnt*AOA_NUM_SAMPLES_PER_BLOCK + a*AOA_NUM_SAMPLES_PER_BLOCK + i].i,
                                          sample[r*numAnt*AOA_NUM_SAMPLES_PER_BLOCK + a*AOA_NUM_SAMPLES_PER_BLOCK + i].q,
                                          sample[r*numAnt*AOA_NUM_SAMPLES_PER_BLOCK + b*AOA_NUM_SAMPLES_PER_BLOCK + i].i,
                                          sample[r*numAnt*AOA_NUM_SAMPLES_PER_BLOCK + b*AOA_NUM_SAMPLES_PER_BLOCK + i].q);

// Example code for processing IQ data
int16_t AngleComplexProductComp(int16_t Xre, int16_t Xim, int16_t Yre, int16_t Yim)
{
    int32_t Zre, Zim;
    int16_t angle;

    // X*conj(Y)
    Zre = Xre*Yre + Xim*Yim;
    Zim = Xim*Yre - Xre*Yim;
    Zre >>= 10;
    Zim >>= 10;

    // Angle. The angle is returned in 256/2*pi format [-128,127] values
    angle = iatan2sc((int16_t) Zim, (int16_t) Zre);

    return (angle * angleconst);
}

Something to highlight is that in reality the 250kHz might not be perfect (for example, could be 255kHz or 245kHZ), therefore, there is slightly phase difference between ant_1 sample_n and ant_1 sample_(n + 16*1). Therefore compensation should be applied and we use the same method as before:

Listing 91. Compensation method.
1
2
3
4
5
// Calculate the phase drift across one antenna repetition (X * complex conjugate (Y))
int16_t Paa_rel = AngleComplexProductComp(sample[r*numAnt*AOA_NUM_SAMPLES_PER_BLOCK     + a*AOA_NUM_SAMPLES_PER_BLOCK + i].i,
                                          sample[r*numAnt*AOA_NUM_SAMPLES_PER_BLOCK     + a*AOA_NUM_SAMPLES_PER_BLOCK + i].q,
                                          sample[(r-1)*numAnt*AOA_NUM_SAMPLES_PER_BLOCK + a*AOA_NUM_SAMPLES_PER_BLOCK + i].i,
                                          sample[(r-1)*numAnt*AOA_NUM_SAMPLES_PER_BLOCK + a*AOA_NUM_SAMPLES_PER_BLOCK + i].q);

Because of the none perfect 250kHz tone, the phase difference is aggregated. Let’s say that every period will have 45 degree of delay. Then when comparing ant_1 sample_n and ant_1 sample_(n+16*1), the aggregated phase difference is 90 degree. But the real phase difference between every period is only 45. Therefore the calculated phase difference must be divided by the number of antennas used, in our case 2.

Listing 92. Phase difference.
1
versus_avg[a][b] += Pab_rel - ((Paa_rel * abs(a-b)) /numAnt);

Angle Compensation

Under AoA_getPairAngles(), we will acquired the angle based on I and Q data. After that, angle compensation is added. Please see the code below. This is because angle estimation is affected by antenna pairs and frequency. The values p->gain, p->offset and AoA_A1_freqComp/AoA_A2_freqComp are based on lab measurements. Different antenna board design and frequency will give you different p->gain, p->offset and AoA_A1_freqComp/AoA_A2_freqComp.

The following code can be found in AOA.c AoA_getPairAngles(), this is antenna pairs compensation.

Listing 93. Antenna pair compensation
1
2
3
4
5
6
// Write back result for antenna pairs
for (int i = 0; i < numPairs; ++i)
{
    const AoA_AntennaPair *p = &antConfig->pairs[i];
    antResult->pairAngle[i] = (int16_t)((p->sign * antenna_versus_avg[p->a][p->b] + p->offset) * p->gain);
}
../_images/lab_tuning.png

As you can see from the image above, the offset is applied to make sure the data received at 0 degree will derive 0 degree after the calculation and then the slope is changed to make it fit better with all the rest of the angles.

The compensation values for antenna array 1 can be found in ant_array1_config_boostxl_rev1v1.c AoA_AntennaPair pair_A1[]

Listing 94. Values used for compensation
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
AoA_AntennaPair pair_A1[] =
{
   {// v12
    .a = 0,       // First antenna in pair
    .b = 1,       // Second antenna in pair
    .sign = 1,    // Sign for the result
    .offset = 5,  // Measurement offset compensation
    .gain = 1,    // Measurement gain compensation
   },
   {// v23
    .a = 1,
    .b = 2,
    .sign = 1,
    .offset = 0,
    .gain = 1,
   },
   {// v13
    .a = 0,
    .b = 2,
    .sign = 1,
    .offset = 10,
    .gain = 0.50,
   },
};

Followed by antenna pair compensation, we added frequency compensation.

The following code can be found in aoa_receiver.c: static AoAReceiver_estimateAngle().

Listing 95. Values used for compensation
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
switch(AoAReceiver_antA1Result->ch)
{
    case 37: // 2402 MHz
        AoA_A1_freqComp = -10;
        break;
    case 38: // 2426 MHz
        AoA_A1_freqComp = -5;
        break;
    case 39: // 2480 MHz
        AoA_A1_freqComp = 15;
        break;
}


// Calculate AoA for each antenna array
const int16_t AoA_A1 = ((AoAReceiver_antA1Result->pairAngle[0] + AoAReceiver_antA1Result->pairAngle[1]) / 2) + 45 + AoA_A1_freqComp;
const int16_t AoA_A2 = ((AoAReceiver_antA2Result->pairAngle[0] + AoAReceiver_antA2Result->pairAngle[1]) / 2) - 45 - AoA_A2_freqComp;

Time of Flight

Note

Please note that this ToF implementation is an initial release intended for evaluation purposes only. For questions regarding future releases, please contact TI at https://e2e.ti.com/.

ToF is a technique used for secure range bounding by measuring the round trip delay of an RF packet exchange.

This is implemented in a Master-Slave configuration, where the Master sends a challenge and the Slave returns a response after a fixed turn-around time. The Master can then calculate the round trip delay by measuring the time difference between transmission of the challenge and reception of the response, subtracting the (known) fixed turn-around time.

Due to the low-speed nature of a Bluetooth radio when evaluated in a speed-of- light-context, each individual measurement provides only a very coarse result. But by performing many measurements, typically several hundred within a few milliseconds, an average result with much better accuracy can be achieved.

Theory of Operation

Few things are faster than the speed of light, and that speed is known and constant at c. Electromagnetic waves propagate at the speed of light, and thus an RF packet propagates at the speed of light.

Since the speed is constant, this means that the time it takes for a wave to propagate is directly proportional to the distance. To find the distance to an object, we can record the timestamp when we transmit something and compare this to when the reflection is received, divide by two and multiply by c. This is the operating principle of for example RADAR as well.

../_images/tof_reflection.png

Figure 75. Reflected EM wave. If time between transmit and receive is t, then distance d is simply ct/2.

As opposed to RADAR, the reflector in TI ToF is considered active as it does not reflect the outgoing signal meaningfully but instead must actively send out the “reflection”. In this document and our examples, the signal that is sent out is called a PING and the response is called ACK, or sometimes PONG.

There are at least two main challenges when doing this form of measurement:

  • The time the reflector uses between receipt of the PING and transmission of the PONG will affect the measured distance.
  • Light uses 3.3ns to travel one meter is, which means that the tick speed of a clock measuring the time of flight must apparently be at least 303 MHz to get 1m spatial resolution.

The first challenge is overcome in the TI ToF solution by implementing a deterministic turn-around time in the slave/reflector device.

The frequency of the radio’s demodulator in the ToF configuration is 8 MHz, which means that the temporal resolution is 125 ns. The accuracy of the final measurement can be improved by oversampling the individual packet measurements. This is because there is jitter in the sample, and this jitter has a normal distribution.

Packet format

The modulation format is 2Mbps at 250kHz deviation using a piecewise linear shaper for the transitions.

../_images/aafig-1d4e84fb85bf72f8b0298173b4020dfad1a8afcc.png

The packets contain a random pre-shared sync word that is unique for each frame sent over the air, and that either side will be listening for in their RX cycle.

The syncword is how the RF circuitry detects that the packet is intended for the device, and also how the timestamps are generated for the measurements. The randomness is important to prevent replay or guess-based attacks.

ToF Protocol

From the ToF driver’s perspective there must be at least two devices: A Master device and a Slave device. In addition to Master and Slave, a Passive device can also be added into the ToF application to gather more data within the same amount of the time to minimize interference from reflector and to improve the accuracy.

The distance between Passive and Master is fixed and known, therefore a compensation for the Passive and Master can be added during data post-process.

The Master and Slave devices will listen and transmit on a range of frequencies, and using a range of sync-words, both provided by the application. Meanwhile, the Passive device will just listening to all the packets transmitted between Master and Slave after these two have synced up.

The Slave is initially in receive mode on first frequency and is listening for the first syncword Sw0 which is in the array provided by application.

If the slave receives a matching packet in the initial listening period it will send an ACK/PONG and will start following a time-slotted scheme where it changes frequency and syncwords according to the list provided.

The master will send out a packet containing the first syncword(Sw0) and some application defined payload. Immediately after this it will go into receive mode and wait for a slave to reply transmitting the second syncword(Sw1) in the array.

Similarly, if the master receives a matching ACK/PONG packet in response to the initial PING, it will follow the same time-slotted and frequency- hopping scheme.

During the initial sync process, Passive does not participate the transaction at all. Once Passive sniff Sw2, it will start following the time-slotted and frequency hopping scheme. Passive starts the radio timer when sniffs Sw2n, and then stop the timer once Passive sniffs Sw2n+1.

@startuml
participant  "Master device" as Master
participant "Passive device" as Passive
participant "Slave device" as Slave


== Initial Sync ==

rnote over Passive
RX: waiting
for Sw2
end rnote
Master -> Slave: Sw<sub>0</sub> Freq<sub>0</sub> PING
activate Master
Master -> Master: Sw<sub>1</sub>
Master -> Master: No Sync
deactivate Master


Slave -> Slave: Enter initial\nRX: Sw<sub>0</sub> + Freq<sub>0</sub>
activate Slave
Master -> Slave: Tx: Sw<sub>0</sub> Freq<sub>0</sub> PING
activate Master
Master -> Master: Sw<sub>1</sub> RX
Slave -> Master: Sw<sub>1</sub> PONG
deactivate Slave
deactivate Master


== Time-slotted ==

note over Master, Slave
   From here the devices will go to the next
   channel even if no sync is received.
end note

Slave -> Slave: Sw<sub>2</sub> + Freq<sub>1</sub> RX
activate Slave

Master -> Slave: Sw<sub>2</sub> Freq<sub>1</sub> PING

activate Passive
rnote over Passive
Start timer
end rnote
activate Master
Master -> Master: Sw<sub>3</sub> RX

Slave -> Master: Sw<sub>3</sub> Freq<sub>1</sub> PONG
rnote over Passive
Stop timer
end rnote
deactivate Slave
deactivate Master
deactivate Passive

loop

Slave -> Slave: Sw<sub>n</sub> + Freq<sub>m</sub> RX
activate Slave
Master -> Slave: Sw<sub>n</sub> Freq<sub>m</sub> PING
activate Passive
rnote over Passive
Start timer
end rnote
activate Master
Master -> Master: Sw<sub>n+1</sub> RX
Slave -> Master: Sw<sub>n+1</sub> Freq<sub>m</sub> PONG
rnote over Passive
Stop timer
end rnote
deactivate Slave
deactivate Master
deactivate Passive
end
@enduml

Figure 76. ToF measurement-burst sequence

If we consider the timing of the packets, we can try to illustrate the time of flight as the distance between the two first vertical lines below.

You can see that the internal clocks of the two devices are not syncronized, illustrated by the top line for each device, and you can see the three phases of a ToF measurement:

  1. Master sends PING, Slave receives
  2. Devices switch RF roles, TX to RX and RX to TX
  3. Response PONG is sent
../_images/tof_timing.png

Figure 77. ToF Timing diagram. See legend below.

TA - Master sends challenge PING, TB - Master TX/RX switch, TC - Master detects Sw correlation, TD - Slave detects Sw correlation, TE - Slave TX/RX switch, TF - Slave sends response PONG,

Application

The application has to somehow agree with the peer device(s) what frequencies should be used, the order of frequencies, and what the list of syncwords should contain.

In addition, it must call ToF_run(…) at appropriate times to initialize the initial syncword search and the time slotted measurement burst.

This can also be shown as a (very) simplified sequence diagram:

@startuml
participant Application as app
participant ToF_driver as drv
participant RF_driver as rf
participant Radio as radio

activate app
app -> app : Initialize\nToF_Params

app -> drv : ToF_open(..)
activate drv
drv -> drv : Initialize
drv -> rf : RF_open(..)
drv -> app : ToF_Handle
deactivate drv
deactivate app

...

app -> drv : ToF_run(freqs, ...)
activate drv
drv -> rf : RF_schedCmd
deactivate drv
activate rf
rf -> radio : Load patch
activate radio
rf -> radio : Run command
deactivate rf

radio -> radio : Search for\nsync
radio --> : Sw<sub>0</sub>
radio <-- : Sw<sub>1</sub>


radio -> radio : Loop until finished\nand store timestamps

radio -> rf : Interrupt
deactivate radio

rf -> drv : Callback
drv -> app : Callback

activate app
app -> drv : ToF_getBurstStats(..)

drv -> drv : Calculate stats\nfrom timestamps

drv -> app : ToF_BurstStats

app -> app : Average a bit

[<- app : Display
@enduml

Figure 78. Application use of ToF

Implementation

From the application, it is relatively simple. You need to:

  1. Initialize
  2. Run
  3. Collect the results
  4. Calibrate

Initialize

The ToF driver needs a parameter struct with some information filled in. The example has this filled in already, but the most interesting parameters are:

  • tofRole - Device role: Master/Slave
  • pT1RSSIBuf - Pointer to sample buffer
  • pSyncWords - Pointer to array of syncwords
  • numBurstSamples - Number of syncwords
  • frequencies - Pointer to array of frequencies
  • numFreq - Number of frequencies
  • pfnTofApplicationCB - Callback after run. Function takes uint8_t status.

The sample buffer must be at least numBurstSamples / 2 large. The list of frequencies and the list of syncwords must be identical on both sides.

You must also allocate space for the ToF driver instance, a struct of the type ToF_Struct.

Once that’s done, you can call ToF_Handle handle = TOFCC26XX_open(&tofStruct, &tofParams);

Run

When you call TOFCC26XX_run(handle, tofEndTime) it will start immediately. When combined with Bluetooth, you will get the time in Radio Access Timer (RAT) ticks from the BLE Stack to use as ToF end-time.

If you are running standalone, an absolute timestamp is still needed, but one can be finessed by using RF_getCurrentTime() from ti/drivers/RF/h and adding 4 * <uSec> to that.

Collect the results

This is done in the callback function given in the initialization parameters:

ToF_BurstStat tofBurstResults[TOF_MAX_NUM_FREQ] = {0};

void myCallback(uint8_t status) {
  TOFCC26XX_getBurstStat(tofHandle, tofBurstResults);
  // Do something
}

This function takes the interleaved raw samples and averages them per frequency.

../_images/aafig-5d67669dc1ec4fd6a1b6808790ee46c5b9bb070d.png

The raw samples are stored in a flat list (above), but the statistics function presents them per frequency:

typedef struct
{
    uint16_t freq;          // Frequency
    double   tick;          // Time of Flight in clock ticks, averaged over all valid samples for `freq`
    double   tickVariance;  // Variance of the tick values
    uint32_t numOk;         // Number of packets received OK for `freq`
} ToF_BurstStat;

The tick value can be converted to meters by subtracting the calibrated tick value at 0 meters and multiplying with 18.75.

The scaling factor is due to the resolution of the radio timer, which is 8 MHz, and the time it takes for light to travel 1 meter two times (back and forth).

\begin{align*}
\begin{split}
\text{distance} &= \frac{1}{2}\times\text{t}\times\text{c}\\[16pt]
&= \frac{1}{2}\times\frac{\text{tick}}{8\si{\mega\hertz}}\times\text{c}\\[16pt]
&= \frac{\text{tick}}{2\times24\times10^{6}\text{s}^{-1}} \times 3\times10^{8}\si{\meter/\second}\\[16pt]
&= 18.75\si{\meter}\times\text{tick}
\end{split}
\end{align*}

Note

For more software detail please visit tof_inintiator readme

Calibrate

Due to the deterministic but non-zero turn-around time in the slave, it is necessary to record the average tick value per frequency at a known distance, and then later subtract the calibrated tick values from the measured result and add back the calibration distance.

Interleaving with Bluetooth

A Bluetooth LE device in a connection is bound to wake up and transmit or receive at certain times known as Connection Events. The interval between such events is called a Connection Interval.

The Passive device is integrated with connection monitor functionality, therefore, it can obtain connection information(rssi, timestamp) and also listens for ToF packets at the end of each connection event and providing calculated distance.

../_images/ditaa-ba8e1bb8cade777ce1d6e45d49ed954d12e43ad6.png

In the time between the end of one connection event and the start of the next scheduled connection event, there is time schedule other RF commands.

The available time can vary, but a timestamp is provided in the connection event complete callback for when Time of Flight has to be finished using the radio.