Introduction

Note

Please note that this lab only applies to SDK 3.30.00. We are in the process of revamping our RTLS SimpleLink Academy Lab content and plan to release an update for SDK 3.40.00.

For updated documentation RTLS for SDK 3.40.00 please refer to the TI BLE-Stack User's Guide.

Please also note that the RTLS Agent has moved and is now found in the <SIMPLELINK_CC2640R2_SDK_INSTALL_DIR>\tools\blestack\rtls_agent folder. For more info, see the RTLS example project readme files.

This module is a study of the Angle of Arrival (AoA). AoA is a technique for finding the direction that an incoming packet is coming from. The estimated time to complete this lab is between 1-2 hours. It is assumed that the reader has a basic knowledge of embedded C tool chains and general C programming concepts.

This lab is based on the rtls_master, rtls_slave and rtls_passive projects that are part of the SimpleLink™ CC2640R2 SDK.

First, the lab will cover an overview on how to get started with the AoA projects. Subsequent tasks of this lab will guide the user on how to customize these projects in order to answer most of the common AoA questions.

Prerequisites

Required Training

It is required that users complete all steps of the RTLS Introduction lab before moving on to the Angle of Arrival lab. It is also assumed that the user has already installed the software, setup the hardware, and read the recommended chapters covered by the introduction lab. For brevity, only new content will be listed here.

Software for desktop development

This module requires the following kits:

Getting started with AoA booster pack

Getting started – Desktop

Install the Software

This lab requires that you have completed all the steps in the RTLS Introduction lab This means that you must have

  • CC2640R2 SDK installed
  • Working Python environment
  • Used rtls_example.py script that sets up an RTLS network.

Introduction to AoA

Bluetooth Core Specification Version 5.1 introduces AoA/AoD which are covered under Direction Finding Using Bluetooth Low Energy section.

AoA is for receivers that have RF switches, multiple antennas, can switch antennas and capture I/Q samples when receiving direction finding packets.

AoD is for transmitters that have RF switches, multiple antennas and can switch antennas when transmitting direction finding packets. And recieivers only have one antenna but can capture I/Q samples when receiving direction finding packets.

In the table below and throughout this lab CTE is used in place of Constant Tone Extension as defined by the BLE specification.

Direction Finding Method Transmitter Receiver
AoA Single antenna, transmit CTE Multiple antennas, RF switches, can switch antennas while capturing I/Q data of the CTE
AoD Multiple antennas, RF switches, transmits CTE while switching antennas Single antenna, can capture I/Q data of the CTE

On top of that, the Bluetooth Core Specification version 5.1 specifies the following states can support sending direction finding packets:

  1. Periodic advertising; also called Connectionless CTE
  2. Connection; also called Connection CTE

The theory behind AoA/AoD and Connectionless/Connection CTE is the same, therefore in this SimpleLink Academy training, we will only focus on Connection CTE AoA.

We will explain the AoA theory first and the walk through our SimpleLink CC2640R2F SDK offering.

AoA measurement is typically a 3-step process:

  1. Collect phase information by sampling the I/Q
  2. Calculate the phase difference among the antennas
  3. Covert the phase difference into Angle of Arrival

1. Collect phase information by sampling the I/Q

When two (or more) antennas are placed at a given distance apart from each other, their received RF signals will have a phase difference that is proportional to the difference between their respective distances from the transmitter.

Typically, the signal from one antenna will be a delayed version of the signal from the other antenna. If they are sufficiently close (less than Λ(wavelength)/2 between phase centers), you can always determine which is closest to the transmitter.

These path length differences will depend on the direction of the incoming RF waves relative to the antennas in the array. In order to measure the phase difference accurately, the radio wave packet must contain a section of constant tone with whitening disabled where there are no phase shifts caused by modulation.

Constant Tone Extension (CTE)

The constant tone extension is a section of consecutive 1's without whitening, which is effectively a +250kHz wave on top of the carrier wave. In the Bluetooth Core Specification Version 5.1, both periodic advertising packets and connection packets can contain a constant tone extension(CTE) after the CRC. The CTE can only be sent using uncoded PHY.

2. Calculate the phase difference among the antennas

Phase Difference (Φ) is measured by connecting at least two antennas to the same receiver sequentially (more antennas can be added).

The picture shown below is a constellation diagram which illustrates signal vectors from 2 antennas. If all the antennas are positioned in line and with a fixed distance d, the phase difference Φ between adjacent antennas will be constant.

In order to get a good estimate of Φ (phase), all other intentional phase shifts in the signal should be removed. Connection CTE AoA solution achieves this by adding CTE at the end of packets.

Connection CTE Packet Format

Data Physical Packet format

The grey colored part of the packet shown above means it's optional. The Constant Tone Extension is only enabled when the CP bit in the Data Physical PDU header is set. The detail of the Constant Tone Extension is then specified by the CTEInfo field in the in the Data Physical PDU.

This gives the receiver time to synchronize the demodulator first, and then store the I/Q samples from the CTE into radio RAM. The I/Q data is then extracted by the application layer.

I/Q samples are used to estimate phase difference among antennas. When the receiver gets AoA packets, the RF core will trigger an event that will lead to start of the antenna switching. The RF core will start sampling the I/Q data after the guard period of the CTE and the sampled data will be stored in the radio RAM.

By comparing the I/Q data collected from different antennas, users can get the relative phase difference among antennas.

3. Convert the phase difference into Angle of Arrival

Last step is converting the phase shift (Φ) back to AoA (Θ). If Φ is negative, this means that antenna 2 is ahead of antenna 1. In this case Θ is negative too but this does not cause any mathematical problem as sin() and arcsin() functions are defined both for positive and negative numbers. To avoid any unnecessary complications we will consider here Φ to be positive.

The angle between the incident wave and antenna array is Θ. Base on the picture below we know that the sin(Θ) = r/d, and d is the distance between antenna 1 and antenna 2 which is known. Then all we need to find out is r.

r is the distance to antenna 2 that the incident wave needs to travel after arriving at antenna 1. We have found that the phase difference between antenna 1 and antenna 2 is Φ, so the extra distance r is equal to wavelength of the incoming signal * Φ/(2Π).

r= Λ* Φ/(2Π)

Therfore Θ can be represented as below:

Note

In rtls_passive example, you might notice that step 3 is not implemented. The reason is simply because by step 2 using our BOOSTXL-AoA we already have linear result from +/-90 degree using antenna pair[1,2] and pair[2,3]. Therefore all we need is to have a look up table to do the fine tuning.

This saves computation time/current consumption compared to doing another arcsin function.

We also tried to do arcsin, but the result is pretty much the same as adding gain and offset after step 2.

What technique does AoA use to identify the direction of incident wave?

Task 1 – Running the AoA application

Have you moved the capacitor C51 of your LaunchPad to use external antenna?

If you have no idea what I am talking about, it's time for you to read the "Preparations" chapter of the Angle of Arrival BoosterPack Getting Started guide :)

The SimpleLink™ CC2640R2 SDK has three examples dedicated to performing AoA (rtls_master, rtls_slave and rtls_passive).

In the TI BLE-Stack User's Guide → RTLS Toolbox → Angle of Arrival → AoA application Overview, we have covered how the application works.

Therefore, after following the instruction on Realtime Localization System Introduction, you should be able to get it up and running.

Task 2 – Modify AoA application to use only one antenna array

The out of box SimpleLink™ CC2640R2 SDK rtls_passive example will stay on one antenna array when receiving one AoA packet and then change to another antenna array for the next AoA packet. Following code contains the logic for antenna array switching.

  // Switch ant array
  if (gAoaReport.antConfig == antA1Config)
  {
    gAoaReport.antConfig = antA2Config;
    gAoaReport.antResult = antA2Result;
  }
  else if (gAoaReport.antConfig == antA2Config)
  {
    gAoaReport.antConfig = antA1Config;
    gAoaReport.antResult = antA1Result;
  }
  else
  {
    // Should not get here
    return;
  }

AOA.c::AOA_setupNextRun()

The angle is then decided once both arrays have received AoA packets and judge by the rssi value of the received packet to decide whether the application should trust angle derived from array 1 or array 2.

    // Signal strength is higher on A1 vs A2
    if (antA1Result->rssi > antA2Result->rssi)
    {
        // Use AoA from Antenna Array A1
        AoA_ma.array[AoA_ma.idx] = AoA_A1;
        AoA_ma.currentAoA = AoA_A1;
        AoA_ma.currentAntennaArray = 1;
        AoA_ma.currentRssi = antA1Result->rssi;
        AoA_ma.currentSignalStrength = signalStrength_A1;
        AoA_ma.currentCh = antA1Result->ch;
        AoA.currentangle = AoA_A1;
    }
    // Signal strength is higher on A2 vs A1
    else
    {
        // Use AoA from Antenna Array A2
        AoA_ma.array[AoA_ma.idx] = AoA_A2;
        AoA_ma.currentAoA = AoA_A2;
        AoA_ma.currentAntennaArray = 2;
        AoA_ma.currentRssi = antA2Result->rssi;
        AoA_ma.currentSignalStrength = signalStrength_A2;
        AoA_ma.currentCh = antA2Result->ch;
        AoA.currentangle = AoA_A2;
    }

rtls_ctrl_aoa.c::RTLSCtrl_estimateAngle()

However, this might not be how you want to design or utilize your antennas. Therefore, this section we will walk you through the changes if you only want one antenna array, like Automotive Bluetooth low energy car access satellite node reference design

  • We will go from this configuration:

  • To this configuration (using antenna array #1):

The steps below will guide you through the process of removing one of the antenna array. For this example we will be removing A2, but these steps are also applicable in case that A1 needs to be removed.

  1. Start from the rtls_passive_cc2640r2lp_app project and rename it to rtls_passive_cc2640r2lp_a1_app
  2. Exclude the files ant_array2_config_boostxl_rev1v1.c and ant_array2_config_boostxl_rev1v1.h from the build.
  3. Comment out all of the antenna array 2 dependencies from AOA.c
  //Comment out all the followings lines
  //line 64
  #include "ant_array2_config_boostxl_rev1v1.h"

  // Remove config and result declarations line ~170
  AoA_AntennaConfig *antA2Config = &BOOSTXL_AoA_Config_ArrayA2;
  AoA_AntennaResult *antA2Result = &BOOSTXL_AoA_Result_ArrayA2;

  //line 399, 402 and 405 in AOA_init()
  BOOSTXL_AoA_AntennaPattern_A2_init();
  antA2Result->updated = false;
  aoaResults->antA2Result = antA2Result;

  //line 730~733 in AOA_getActiveAnt()
  else if (gAoaReport.antConfig == antA2Config)
  {
    return 2;
  }

  //line 756~760 in AOA_setupNextRun()
  else if (gAoaReport.antConfig == antA2Config)
  {
    gAoaReport.antConfig = antA1Config;
    gAoaReport.antResult = antA1Result;
  }
  1. Change the following lines in AOA.c to take antenna 1 instead.

    //line 753 and 754 in AOA_setupNextRun()
    // Switch ant array
    if (gAoaReport.antConfig == antA1Config)
    {
     gAoaReport.antConfig = antA1Config;
     gAoaReport.antResult = antA1Result;
    }
    

    c

  2. Replace the content of RTLSCtrl_postProcessAoa in rtls_ctrl_aoa.c with the provided code snippet. What we have done here is to remove all the antenna array 2 dependency.

    void RTLSCtrl_postProcessAoa(rtlsAoa_t *aoaControlBlock, int8_t rssi, uint8_t channel)
    {
    uint8_t status;
    
    if (aoaControlBlock->aoaParams.aoaRole == AOA_ROLE_PASSIVE)
    {
     status = AOA_postProcess(rssi, channel);
    
     if (status)
     {
       switch(aoaControlBlock->aoaParams.resultMode)
       {
         case AOA_MODE_ANGLE:
         {
           AoA_Sample aoaTempResult;
           rtlsAoaResultAngle_t aoaResult;
    
           AOA_getPairAngles();
    
           {
             gAoaResults.antA1Result->updated = false;
    
             const AoA_AntennaResult *antA1Result = gAoaResults.antA1Result;
    
             aoaTempResult = RTLSCtrl_estimateAngle(antA1Result);
    
             aoaResult.angle = aoaTempResult.angle;
             aoaResult.antenna = aoaTempResult.antenna;
             aoaResult.rssi = aoaTempResult.rssi;
             aoaResult.channel = aoaTempResult.channel;
    
             RTLSHost_sendMsg(RTLS_CMD_AOA_RESULT_ANGLE, HOST_ASYNC_RSP, (uint8_t *)&aoaResult, sizeof(rtlsAoaResultAngle_t));
           }
         }
         break;
    
         case AOA_MODE_PAIR_ANGLES:
         {
           rtlsAoaResultPairAngles_t aoaResult;
           int16_t *pairAngle;
           uint8_t antenna;
    
           AOA_getPairAngles();
    
           if (gAoaResults.antA1Result->updated == true)
           {
             pairAngle = gAoaResults.antA1Result->pairAngle;
             gAoaResults.antA1Result->updated = false;
             antenna = 1;
           }
           else
           {
             // Should not get here
             return;
           }
    
           aoaResult.rssi = rssi;
           aoaResult.channel = channel;
           aoaResult.antenna = antenna;
    
           for (int i = 0; i < AOA_NUM_ANTENNAS; i++)
           {
             aoaResult.pairAngle[i] = pairAngle[i];
           }
    
           RTLSHost_sendMsg(RTLS_CMD_AOA_RESULT_PAIR_ANGLES, HOST_ASYNC_RSP, (uint8_t *)&aoaResult, sizeof(rtlsAoaResultPairAngles_t));
         }
         break;
    
         case AOA_MODE_RAW:
         {
           rtlsAoaResultRaw_t *aoaResult;
           uint16_t numAoaSamples;
           uint16_t samplesToOutput;
           AoA_IQSample *pIter;
    
           aoaResult = RTLSCtrl_malloc(sizeof(rtlsAoaResultRaw_t) + (MAX_SAMPLES_SINGLE_CHUNK * sizeof(AoA_IQSample)));
           pIter = AOA_getRawSamples();
    
           // If the array is not empty
           if (pIter != NULL && aoaResult != NULL)
           {
             // Calculate how many samples we will be outputting
             numAoaSamples = AOA_calcNumOfCteSamples(aoaControlBlock->aoaParams.cteTime,
                                                     aoaControlBlock->aoaParams.cteScanOvs,
                                                     aoaControlBlock->aoaParams.cteOffset);
    
             aoaResult->channel = channel;
             aoaResult->rssi = rssi;
             aoaResult->samplesLength = numAoaSamples;
             aoaResult->antenna = AOA_getActiveAnt();
             aoaResult->offset = 0;
    
             do
             {
               // If the remainder is larger than buff size, tx maximum buff size
               if (aoaResult->samplesLength - aoaResult->offset > MAX_SAMPLES_SINGLE_CHUNK)
               {
                 samplesToOutput = MAX_SAMPLES_SINGLE_CHUNK;
               }
               else
               {
                 // If not, then output the remaining data
                 samplesToOutput = aoaResult->samplesLength - aoaResult->offset;
               }
    
               // Copy the samples to output buffer
               for (int i = 0; i < samplesToOutput; i++)
               {
                 aoaResult->samples[i] = pIter[i];
               }
    
               RTLSHost_sendMsg(RTLS_CMD_AOA_RESULT_RAW, HOST_ASYNC_RSP, (uint8_t *)aoaResult, sizeof(rtlsAoaResultRaw_t) + (sizeof(AoA_IQSample) * samplesToOutput));
    
               aoaResult->offset += samplesToOutput;
               pIter += MAX_SAMPLES_SINGLE_CHUNK;
             }
             while (aoaResult->offset < aoaResult->samplesLength);
           }
    
           if (aoaResult)
           {
             RTLSUTIL_FREE(aoaResult);
           }
         }
         break;
    
         default:
           break;
       }
       // Once all post processing is complete, set up the next run
       AOA_setupNextRun();
     }
    }
    }
    

    rtls_ctrl_aoa.c:: RTLSCtrl_postProcessAoa

  3. Replace the content of RTLSCtrl_estimateAngle in rtls_ctrl_aoa.c with the provided code snippet. What we have done here is to remove all the antenna array 2 dependency and modify the function call to take one array only when making an angle decision.

    //line 122 under local function section
    AoA_Sample RTLSCtrl_estimateAngle(const AoA_AntennaResult *antA1Result);
    
    // The function itself
    AoA_Sample RTLSCtrl_estimateAngle(const AoA_AntennaResult *antA1Result)
    {
     AoA_Sample AoA;
    
     static AoA_movingAverage AoA_ma;
     uint8_t AoA_ma_size = sizeof(AoA_ma.array) / sizeof(AoA_ma.array[0]);
    
     // Calculate AoA for each antenna array
     const int16_t AoA_A1 = ((antA1Result->pairAngle[0] + antA1Result->pairAngle[1]) / 2) + antA1Result->channelOffset[antA1Result->ch];
     // Calculate average signal strength
     const int16_t signalStrength_A1 = (antA1Result->signalStrength[0] + antA1Result->signalStrength[1]) / 2;
    
     // Use AoA from Antenna Array A1
     AoA_ma.array[AoA_ma.idx] = AoA_A1;
     AoA_ma.currentAoA = AoA_A1;
     AoA_ma.currentAntennaArray = 1;
     AoA_ma.currentRssi = antA1Result->rssi;
     AoA_ma.currentSignalStrength = signalStrength_A1;
     AoA_ma.currentCh = antA1Result->ch;
     AoA.currentangle = AoA_A1;
    
     // Add new AoA to moving average
     AoA_ma.array[AoA_ma.idx] = AoA_ma.currentAoA;
    
     // Calculate new moving average
     AoA_ma.AoAsum = 0;
     for(uint8_t i = 0; i < AoA_ma_size; i++)
     {
         AoA_ma.AoAsum += AoA_ma.array[i];
     }
     AoA_ma.AoA = AoA_ma.AoAsum / AoA_ma_size;
    
     // Update moving average index
     if(AoA_ma.idx >= (AoA_ma_size - 1))
     {
         AoA_ma.idx = 0;
     }
     else
     {
         AoA_ma.idx++;
     }
    
     // Return results
     AoA.angle = AoA_ma.AoA;
     AoA.rssi = AoA_ma.currentRssi;
     AoA.signalStrength = AoA_ma.currentSignalStrength;
     AoA.channel = AoA_ma.currentCh;
     AoA.antenna =  AoA_ma.currentAntennaArray;
    
     return AoA;
    }
    

    rtls_ctrl_aoa.c::RTLSCtrl_estimateAngle

When complete, re-run the rtls_example_with_rtls_util.py, you will observe that all results show antenna 1. See below for example. Note the device address will be different for your launchpad.

[10:18:2019 10:08:13:829688] : {"name": "CC2640r2 AOA Passive", "type": "aoa", "identifier": "54:6C:0E:9F:D4:25", "payload": {"angle": 35, "rssi": -56, "antenna": 1, "channel": 7}}
[10:18:2019 10:08:13:982572] : {"name": "CC2640r2 AOA Passive", "type": "aoa", "identifier": "54:6C:0E:9F:D4:25", "payload": {"angle": 34, "rssi": -56, "antenna": 1, "channel": 0}}
[10:18:2019 10:08:14:184615] : {"name": "CC2640r2 AOA Passive", "type": "aoa", "identifier": "54:6C:0E:9F:D4:25", "payload": {"angle": 35, "rssi": -57, "antenna": 1, "channel": 30}}
[10:18:2019 10:08:14:385083] : {"name": "CC2640r2 AOA Passive", "type": "aoa", "identifier": "54:6C:0E:9F:D4:25", "payload": {"angle": 34, "rssi": -56, "antenna": 1, "channel": 8}}
[10:18:2019 10:08:14:587084] : {"name": "CC2640r2 AOA Passive", "type": "aoa", "identifier": "54:6C:0E:9F:D4:25", "payload": {"angle": 35, "rssi": -58, "antenna": 1, "channel": 16}}
[10:18:2019 10:08:14:789337] : {"name": "CC2640r2 AOA Passive", "type": "aoa", "identifier": "54:6C:0E:9F:D4:25", "payload": {"angle": 34, "rssi": -56, "antenna": 1, "channel": 9}}
[10:18:2019 10:08:14:991393] : {"name": "CC2640r2 AOA Passive", "type": "aoa", "identifier": "54:6C:0E:9F:D4:25", "payload": {"angle": 35, "rssi": -56, "antenna": 1, "channel": 2}}
[10:18:2019 10:08:15:194291] : {"name": "CC2640r2 AOA Passive", "type": "aoa", "identifier": "54:6C:0E:9F:D4:25", "payload": {"angle": 35, "rssi": -56, "antenna": 1, "channel": 32}}
[10:18:2019 10:08:15:395712] : {"name": "CC2640r2 AOA Passive", "type": "aoa", "identifier": "54:6C:0E:9F:D4:25", "payload": {"angle": 34, "rssi": -57, "antenna": 1, "channel": 10}}
[10:18:2019 10:08:15:597805] : {"name": "CC2640r2 AOA Passive", "type": "aoa", "identifier": "54:6C:0E:9F:D4:25", "payload": {"angle": 35, "rssi": -57, "antenna": 1, "channel": 3}}

Task 3 – Modify AoA application to use only two antennas

Now we have successfully moved from using 2 antenna arrays to 1. We will use the example from Task 2 as the baseline and make the proper changes so we only use 2 antennas from the antenna array 1.

  • Task 2 is using all 3 antennas from A1:

  • And the end goal is using the following:

The following steps will guide you through this process:

  1. Comment out the following in ant_array1_config_boostxl_rev1v1.c (the file is in Drivers/AOA).

     //line 58
     #define AOA_Ax_ANT3    AOA_PIN(IOID_30)
    
     //line 131~144
     {// v23
      .a = 1,
      .b = 2,
      .sign = 1,
      .offset = -5,
      .gain = 0.9,
     },
     {// v13
      .a = 0,
      .b = 2,
      .sign = 1,
      .offset = -20,
      .gain = 0.50,
     },
    
  2. Replace the antenna toggling patten antennaPattern_A1 in ant_array1_config_boostxl_rev1v1.c with the provided pattern to remove A1.3.

    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_ANT1, // A1.1
      AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
      AOA_A1_SEL | AOA_Ax_ANT1, // A1.1
      AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
    
      AOA_A1_SEL | AOA_Ax_ANT1, // A1.1
      AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
      AOA_A1_SEL | AOA_Ax_ANT1, // A1.1
      AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
      AOA_A1_SEL | AOA_Ax_ANT1, // A1.1
      AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
    
      AOA_A1_SEL | AOA_Ax_ANT1, // A1.1
      AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
      AOA_A1_SEL | AOA_Ax_ANT1, // A1.1
      AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
      AOA_A1_SEL | AOA_Ax_ANT1, // A1.1
      AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
    
      AOA_A1_SEL | AOA_Ax_ANT1, // A1.1
      AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
      AOA_A1_SEL | AOA_Ax_ANT1, // A1.1
      AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
      AOA_A1_SEL | AOA_Ax_ANT1, // A1.1
      AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
    
      AOA_A1_SEL | AOA_Ax_ANT1, // A1.1
      AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
      AOA_A1_SEL | AOA_Ax_ANT1, // A1.1
      AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
      AOA_A1_SEL | AOA_Ax_ANT1, // A1.1
      AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
    
      AOA_A1_SEL | AOA_Ax_ANT2, // A1.2 stay with antenna 2 at the end
      AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
     }
    };
    

    Using different antennas instead of A1.1 and A1.2

    Our example code always uses antenna A1.2 or A2.2 to receive packets before the CTE. This is set by the code below

       .initialPattern = AOA_A1_SEL | AOA_Ax_ANT2,
    

    antennaPattern_A1[] :: ant_array1_config_boostxl_rev1v1.c

    Therefore, for users using antennas other than A1.2 or A2.2, you will need to modify the initialPattern too.

  3. Change the AOA_NUM_ANTENNAS to 2 in AOA.h

    // line 74
    #define AOA_NUM_ANTENNAS 2
    
  4. The IOs that control the antenna switches are initialized in AOA.c::AOA_openPins Therefore, we need to modify this part of the code since we are using one less IO. Change the pinMask to the following

    //line 297 and line 323
    pinMask = (1 << IOID_27 | 1 << IOID_28 | 1 << IOID_29 );
    

Fundamental done

Now the application only uses antenna A1.1 and A1.2! Very well done! And the updated software still works with RTLS_Monitor GUI and the python scripts provided

Task 4 – Export raw IQ samples to CSV file

Python solution

The goal of this task is to understand how the script rtls_aoa_iq_with_rtls_util_export_into_csv.py (stored in your SDK under tools\ble5stack\rtls_agent\examples)is made. We will guide you step by step in order to rewrite it. You can directly jump to the next task if you are not interested in this part... however it is always cool to discover how the magic happens :)

1- Where does the I/Q Samples come from?

So far we have worked on the application code to modify the rtls_passive to work with custom HW. We will now spend some time looking into the I/Q data.

The reason for checking I/Q data is that different HW design will yield different result which will affect your own angle calculation algorithm. For example, if the rf switches you are using need longer settling time than the ones on BOOSTXL-AoA, then you will need to discard more I/Q samples when doing calculations.

To decide how many I/Q samples you can use, you will need to extract I/Q samples and calculate phase to determine at which samples antennas are changing.

We will first take a look at where I/Q samples are stored and in what format.

I/Q is stored in the radio core RAM(RFC_RAM_BASE + 0x0000C000) and each I/Q pair takes up 32 bits space where I is stored in the lower 16 bits and Q is stored in higher 16 bits.

void AOA_getRxIQ(uint8_t *packetId, AoA_IQSample **samples)
{
  // Should only be called when RF Core is awake
  *packetId = (*((uint8_t*)AOA_PACKET_RAM));
  *samples = packetId ? (AoA_IQSample *)&HWREG(RFC_RAM_BASE + 0x0000C000):NULL;
}

In AOA.c

I/Q samples resolution

I and Q samples only have 13 bits resolution even though they occupy 16 bits space in radio core RAM. Since they only have 13 bits resolution, the maximum and minimum value you will observe as signed integers are [4095, -4096].

2- How to send the I/Q samples to the computer?

  1. If you haven't done it yet, change the COM port used by the script.

         {"com_port": "COM39", "baud_rate": 460800, "name": "CC2640r2 AOA Master"},
         {"com_port": "COM55", "baud_rate": 460800, "name": "CC2640r2 AOA Passive"}
    

    Set up the COM ports used

    To extract the I/Q data from embedded devices over UART, we need to make sure that we have enough time to flush the I/Q data before next AoA packet arrives.

    To achieve this, there are two options:

    • Increase the connection interval. In the rtls_master readme file, it's mentioned that the connection interval should be larger than 300ms to acommodate outputting all the samples.

    • Increase the UART baurate

      For SDK 3.30 and latter, the default UART baudrate is set to 460800. This value is set and can be modified in the function RTLSHost_openHostIf (in the file rtls_host_npi.c, stored in the folder RTLSCtrl.)

            npiPortParams.portParams.uartParams.baudRate = 921600;
      

      Change baudrate (for example) to 921600

      If you modify the baudrate in the embedded code, don't forget to modify the baudrate used by the python script to communicate with the devices...

            {"com_port": "COM39", "baud_rate": 921600, "name": "CC2640r2 AOA Master"},
            {"com_port": "COM55", "baud_rate": 921600, "name": "CC2640r2 AOA Passive"}
      

      Set up the baudrates used

      In this lab we will increase connection interval to 500ms and leave the baudrate as is.

      → Change the connect_interval_mSec parameter to the following code in rtls_example_with_rtls_util.py.

        connect_interval_mSec = 500
      

      Set up connection interval to be 500ms

      This parameter will later be used in the ble_connect API as shown below

        rtlsUtil.ble_connect(slave_bd_addr, connect_interval_mSec)
      

      Post Processing on Embedded Device

      For users that will do I/Q data post process on chip, there is no limitation on the connection interval.

  2. Now we will build on top of the python script that is used in Realtime Localization System Introduction

    First we create a csv file to store I/Q data for analyzing later if needed. Normally, this code is already written in your rtls_aoa_iq_with_rtls_util_export_into_csv.py file, in the function initialize_csv_file. This function assumes that you have properly imported the csv package and declared the global variables csv_writer and csv_row. You can change the filename to whichever suits you better.

         global csv_writer
         global csv_row
    
         # Prepare csv file to save data
         data_time = datetime.datetime.now().strftime("%m_%d_%Y_%H_%M_%S")
         filename = f"{data_time}_rtls_raw_iq_samples.csv"
         outfile = open(filename, 'w', newline='')
    
         csv_fieldnames = ['pkt', 'sample_idx', 'rssi', 'ant_array', 'channel', 'i', 'q']
         csv_row = namedtuple('csv_row', csv_fieldnames)
    
         csv_writer = csv.DictWriter(outfile, fieldnames=csv_fieldnames)
         csv_writer.writeheader()
    

    Create a csv file to log I/Q data

  3. The devices must be reseted, then the master can scan and connect

         ## Reset the passive and the master
         rtlsUtil.reset_devices()
    
         ##...
    
         ## Ask the master to scan
         scan_results = rtlsUtil.scan(scan_time_sec)
    
         ##...
    
         ## Ask the master to connect a device
         rtlsUtil.ble_connect(scan_results[0], connect_interval_mSec)
    

    Set the parameter to configure the devices

  4. Once the slave and the master are connected, we can start the setup of AoA parameters

    To extract I/Q data, the AoA operation mode needs to be AOA_MODE_RAW. If needed, modify in the aoa_params structure, modify the aoa_run_mode to the following:

         aoa_params = {
                         "aoa_run_mode": "AOA_MODE_RAW",
                         "aoa_cc2640r2": {
                             "aoa_cte_scan_ovs": 4,
                             "aoa_cte_offset": 4,
                             "aoa_cte_length": 20
                         },
                         "aoa_cc26x2": {
                             "aoa_slot_durations": 2,
                             "aoa_sample_rate": 4,
                             "aoa_sample_size": 2,
                             "aoa_sampling_control": 0,
                             "aoa_sampling_enable": 1,
                             "aoa_num_of_ant": 3,
                             "aoa_ant_array_switch": 27,
                             "aoa_ant_array": [28, 29, 30]
                         }
                     }
    

    Set the parameter to configure the devices

  5. Let's have a look to the mode that used after a BLE connection has been established. Basically, the data are written to the .csv file until a STOP command is received.

         try:
             data = q.get(block=True, timeout=0.5)
             if isinstance(data, dict):
                 data_time = datetime.datetime.now().strftime("[%m:%d:%Y %H:%M:%S:%f] :")
                 print(f"{data_time} {json.dumps(data)}")
    
                 offset = data['payload'].offset
                 payload = data['payload']
    
                 # If we have data, and offset is 0, we are done with one dump
                 if offset == 0 and len(dump_rows):
                     pkt_cnt += 1
    
                     # Make sure the samples are in order
                     dump_rows = sorted(dump_rows, key=lambda s: s.sample_idx)
    
                     # Write to file
                     for sample_row in dump_rows:
                         csv_writer.writerow(sample_row._asdict())
    
                     # Reset payload storage
                     dump_rows = []
    
                 # Save samples for writing when dump is complete
                 for sub_idx, sample in enumerate(payload.samples):
                     sample = csv_row(pkt=pkt_cnt, sample_idx=offset + sub_idx, rssi=payload.rssi,
                                        ant_array=payload.antenna, channel=payload.channel, i=sample.i, q=sample.q)
                     dump_rows.append(sample)
    
             elif isinstance(data, str) and data == "STOP":
                 print("STOP Command Received")
    
                 # If we have data, and offset is 0, we are done with one dump
                 if len(dump_rows):
                     # Make sure the samples are in order
                     dump_rows = sorted(dump_rows, key=lambda s: s.sample_idx)
    
                     # Write to file
                     for sample_row in dump_rows:
                         csv_writer.writerow(sample_row._asdict())
    
                 break
             else:
                 pass
         except queue.Empty:
             continue
    

    Script's main loop (in results_parsing subfunction)

    Log file

    By default, the script produces a log file. This file is stored in the same path as the .csv file (i.e. in the directory from where the script was run) and can be useful for debug.

  6. After running the script, you can see the following format in the csv file.

Remark 1: The names of the columns when the CSV is iniatized.

    csv_fieldnames = ['pkt', 'sample_idx', 'rssi', 'ant_array', 'channel', 'i', 'q']
    csv_row = namedtuple('csv_row', csv_fieldnames)

    csv_writer = csv.DictWriter(outfile, fieldnames=csv_fieldnames)
    csv_writer.writeheader()

Remark 2: The data written in each column are chosen by the following code.

    # Samples are prepared
    sample = csv_row(pkt=pkt_cnt, sample_idx=offset + sub_idx, rssi=payload.rssi,
                       ant_array=payload.antenna, channel=payload.channel, i=sample.i, q=sample.q)
    dump_rows.append(sample)

    #...

    # Samples are written to the CSV file latter
    for sample_row in dump_rows:
        csv_writer.writerow(sample_row._asdict())

3- How to treat the I/Q samples on the computer?

Here we are only interested in the payload part of the AOA_MODE_RAW command. The AOA_MODE_RAW payload has the following structure. In the python script provided above, we have extracted most of the information and save them to a csv file.

  struct = Struct(
      "rssi" / Int8sl,
      "antenna" / Int8ul,
      "channel" / Int8ul,
      "offset" / Int16ul,
      "samplesLength" / Int16ul,
      "samples" / GreedyRange(Struct(
          "q" / Int16sl,
          "i" / Int16sl,
      )),
  )

ss_rtls.py::class AoaResultRaw

New Python Packages

For the script below you will have to install the following new Python packages

  • pandas
  • matplotlib

They can be installed with pip. Consult the RTLS Intro lab for more information on how to install packages from pip.

import matplotlib.pyplot as plt
import pandas as pd
import math

def cal_magnitude(q_value,i_value):

    return math.sqrt(math.pow(q_value,2)+math.pow(i_value,2))

def cal_phase(q_value,i_value):

    return math.degrees(math.atan2(q_value,i_value))

if __name__ == "__main__":
    df = pd.read_csv('sla_rtls_raw_iq_samples.csv')
    AOA_RES_MAX_SIZE=81

    # We only want one set of data per channel
    df = df.drop_duplicates(subset=['channel','sample_idx'] , keep='first')

    # We Calculate the phase and the margin
    df['phase'] = df.apply(lambda row: cal_phase(row['q'], row['i']), axis=1)
    df['magnitude'] = df.apply(lambda row: cal_magnitude(row['q'], row['i']), axis=1)

    # Plot all the I/Q collected. Each channel will have a plot which contains I/Q samples.
    # If you have collected I/Q data on 37 data channels, then there will be 37 windows popped up
    grouped = df.groupby('channel')
    grouped[['i', 'q']].plot(title="I/Q samples", grid=True)
    plt.show()

    # Create 4 plots and each plot has x number of subplots. x = number of channels
    indexed = df.set_index(['channel', 'sample_idx'])
    indexed.unstack(level=0)[['phase']].plot(subplots=True, title="Phase information", xlim=[0,AOA_RES_MAX_SIZE], ylim=[-190,+190])
    indexed.unstack(level=0)[['magnitude']].plot(subplots=True, title="Signal magnitude", xlim=[0,AOA_RES_MAX_SIZE])
    indexed.unstack(level=0)[['i']].plot(subplots=True, title="I samples", xlim=[0,AOA_RES_MAX_SIZE])
    indexed.unstack(level=0)[['q']].plot(subplots=True, title="Q samples", xlim=[0,AOA_RES_MAX_SIZE])
    plt.show()

This script can be executed as is.

After executing the small script provided above, if the angle between rtls_passive rtls_slave is not 0 degree, you can see that magnitude of the sine wave is not the same when you swap betwen antenna 1 and antenna 2 as shown below.

Another alternative to observe the magnitude of received signal is to calculate it based on I/Q data. Since each I/Q pair is a vector, the magnitude of a vector can be calculated by sqrt(I^2+Q^2).

Using the script provided above, you will see a plot called Signal magnitude. In this plot, you can clear see that received signal magnitude from antenna 2 is almost double as from antenna 1.

More information!

If there is no antenna toggling, then the I/Q samples will form a perfect sine wave. To prove it, change the antenna toggling pattern from 2 antennas to 1 antenna(A1.2), and see how the I/Q plots look like.

AoA_Pattern antennaPattern_A1 = {
    .numPatterns = 32,
    .initialPattern = AOA_A1_SEL | AOA_Ax_ANT2,
    .toggles =
    {
     AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
     AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
     AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
     AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
     AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
     AOA_A1_SEL | AOA_Ax_ANT2, // A1.2

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

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

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

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

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

Task 5 – Antenna Switch Settling Time

Now that we are equipped with all the needed tools to manipulate I/Q data, we will try to identify when the antenna is toggling in order to help determine which I/Q samples can be used.

As mentioned in Calculate the phase difference among the antennas, I/Q samples can be presented with constellation diagram, each I/Q pair is a vector and its angle is within a range of +180 ~ -180. Therefore when we have an I/Q pair, to obtain the angle information, we can use arctan to get angle of a vector. Then based on the I and Q value, we can find out the corresponding angle.

I > 0 I < 0
Q >=0 angle = arctan(Q/I) angle = 180 + arctan(Q/I)
Q < 0 angle = arctan(Q/I) angle = -180 + arctan(Q/I)

Luckily in python, the math.atan2 function takes care of the aforementioned logic.

Let's take a look at the Phase Information plot.

A perfect sine wave, assuming it starts at 0 degree, the phase will first go towards +180 and then, once it crosses +180, it will start from -180 and then go back to 0 degrees. From the picture provided below, you can see that in the first 8us(32 samples), there is no abrupt phase change.

When the rf switches and the angle of incident wave is not 0 degree, you will see abrupt phase change. As shown below after 2 periods, we started to switch antennas. Then the phase we obtained is no long linear.

Hence by looking at the phase information plot, you will know how many I/Q samples you can keep per period.

Well done!

You can now modify the AoA demo to improve it! You can develop your own algorithms and antennas to do cool stuff!

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.