Introduction

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™ CC13X2 / CC26X2 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

Introduction to AoA

Bluetooth Core Specification Version 5.1 introduces AoA/AoD which are covered under Direction Finding Using Bluetooth Low Energy Device 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.

Direction Finding Method Transmitter Receiver
AoA Single antenna, transmit CTE Multiple antennas, RF switches, can switches antennas while capture I/Q data of the CTE
AoD Multiple antennas, RF switches, transmit 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 CC13X2 / CC26X2 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 CC13X2-26X2 SDK 3.30 has three examples dedicated to performing AoA (rtls_master, rtls_slave and rtls_passive).

In the TI BLE5-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.

An important remark before starting

Until the SDK version 3.30, the out-of-the-box example requires three LaunchPad devices (the master, the slave and the passive). The master was “only” responsible to maintain the BLE connection with the slave and to share the connection’s information with the passive device. The passive device was the only responsible to perform the AoA measurements.

Since SDK version 3.30, the master device can perform AoA measurements too. Of course this is only possible if it is equipped with a BOOSTXL-AoA. The master is still responsible to maintain the connection with the slave and to share the connection’s information with the passive device. As a result, you can choose to run the example with:

  • both the master and the passive are performing AoA measurements
  • only the passive is performing the measurements
  • only the master is performing the measurements

In this task, you will configure the example to suit best your needs. In the rest of the lab we will provide the steps assuming you are using both the master and the passive to perform the measurement. If this is not the case, you can basically ignore the steps that do not concern you.

  1. Hardware settings:

    Two BOOSTXL-AoA are required. One BOOSTXL-AoA must be on the passive device, the other must be on the master device.

  2. Embedded Software settings:

    • rtls_passive: The passive's RTLS Control module needs the capability to act as RTLS Passive and AoA receiver. This capabilities are set modifying (if needed) the field rtlsCapab of the rtlsConfig structure (this must be done into the main.c file which is stored under the Startup folder). The capabilities we need for the lab are only RTLS_CAP_RTLS_PASSIVE and RTLS_CAP_AOA_RX.

      rtlsConfig.rtlsCapab = (rtlsCapabilities_e)(RTLS_CAP_RTLS_PASSIVE | RTLS_CAP_AOA_RX);
      

      main.c::main()

      Note: The IOs and the switching pattern used by the antennas are provided through the overrides. We will be back on this point latter.

    • rtls_master: Ensure the master's RTLS Control module has the capability to act as a master (RTLS_CAP_RTLS_MASTER) and to receive the CTE (RTLS_CAP_AOA_RX).

      rtlsConfig.rtlsCapab = (rtlsCapabilities_e)(RTLS_CAP_RTLS_MASTER | RTLS_CAP_AOA_RX);
      

      main.c::main()

      Note: The python script configures the master device in order to select the appropriate IOs and switching pattern. The default parameters are as following:

      "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]
      }
      

      aoa_params used by default by the python script

      The radio core is responsible to handle the antenna toggling. The pattern followed and the IOs to use are given to the radio through overrides.

      Usually, the pyhton script send a command (rtlsUtil.aoa_set_params) to the Master in order to modify the content of the overrides. If you decide not using the python script, then you need to write the overrides yourself.

      Here we will focus on the overrides allowing the radio to handle the CTE. On the Master device, these overrides are stored in the table pOverridesCte in the file ble_user_config.c (under the folder iCallBLE of your project).

      Let's configure it:

            regOverride_t pOverridesCte[] = {
                0x00158000, // S2RCFG: Capture S2R from FrontEnd, on event (CM0 will arm)
                0x000E51D0, // After FRAC
                ((CTE_CONFIG << 16) | 0x8BB3), // Enable CTE capture
                ((CTE_OFFSET << 24) | ((CTE_SAMPLING_CONFIG_1MBPS | (CTE_SAMPLING_CONFIG_2MBPS << 4)) <<    16) | 0x0BC3), // Sampling rate, offset
                0xC0040341, // Pointer to antenna switching table in next entry
                (uint32_t) antSwitching, // Pointer to antenna switching table
                END_OVERRIDE };
      

      ble_user_config.c

      As you can see, we point to an antenna switching table. Let's declare it (in the same file, before the pOverridesCte table):

            #define NUM_ENTRIES  3
            #define SWITCH_TIME  2
      
            #define ANT1         (1<<28)
            #define ANT2         (1<<29)
            #define ANT3         (1<<30)
            #define IO_MASK      (ANT1 | ANT2 | ANT3)
      
            uint32_t antSwitching[] =
            {
              NUM_ENTRIES | (SWITCH_TIME << 8),
              IO_MASK,
              ANT1,
              ANT2,
              ANT3
            };
      

      ble_user_config.c for Master

      For curious people...

      The rtls_passive project also uses overrides to ask the radio to handle the CTE. The overrides used are exactly the same (obvious as the radio core is identical on the passive and the master devices). However the overrides are not stored in the same file. This is because the passive device does not use the BLE stack but the BLE Micro-Stack. On the passive device, the override list can be found in urfc.c:: pOverridesCommon (this file is stored under the Startup directory)

    • rtls_slave: Ensure the slave's RTLS Control module has the capability to act as a slave (RTLS_CAP_RTLS_SLAVE) and to send out the CTE (RTLS_CAP_AOA_TX).

      rtlsConfig.rtlsCapab = (rtlsCapabilities_e)(RTLS_CAP_RTLS_SLAVE | RTLS_CAP_AOA_TX);
      

      main.c::main()

      Once you have done the required modification on the embedded code, flash the LaunchPads and power-cycle them

  3. rtls_agent_cli settings:

    Verify if the passive's and master's capabilities listed by rtls_agent_cli match the settings you have previously done.

         WebSocket Server, ws://localhost:8766           | Waiting for data..
                                                         |
         Connected to 2 of 2 nodes                       |
                                                         |
                                                         |
         * COM12 @ COM12                                 |
             > XX:XX:XX:XX:XX:B4                         |
             > AOA_RX, RTLS_MASTER                       |
                                                         |
         * COM14 @ COM14                                 |
             > XX:XX:XX:XX:XX:F0                         |
             > AOA_RX, RTLS_PASSIVE                      |
    
  1. Hardware settings:

    The BOOSTXL-AoA must be on the passive device.

    • rtls_passive: The passive's RTLS Control module needs the capability to act as RTLS Passive and AoA receiver. This capabilities are set modifying (if needed) the field rtlsCapab of the rtlsConfig structure (this must be done into the main.c file which is stored under the Startup folder). The capabilities we need for the lab are only RTLS_CAP_RTLS_PASSIVE and RTLS_CAP_AOA_RX.

      rtlsConfig.rtlsCapab = (rtlsCapabilities_e)(RTLS_CAP_RTLS_PASSIVE | RTLS_CAP_AOA_RX);
      

      main.c::main()

      Note: The IOs and the switching pattern used by the antennas are provided through the overrides. We will be back on this point latter.

    • rtls_master: Ensure the master's RTLS Control module has the capability to act as a master (RTLS_CAP_RTLS_MASTER).

      rtlsConfig.rtlsCapab = (rtlsCapabilities_e)(RTLS_CAP_RTLS_MASTER);
      

      main.c::main()

    • rtls_slave: Ensure the slave's RTLS Control module has the capability to act as a slave (RTLS_CAP_RTLS_SLAVE) and to send out the CTE (RTLS_CAP_AOA_TX).

      rtlsConfig.rtlsCapab = (rtlsCapabilities_e)(RTLS_CAP_RTLS_SLAVE | RTLS_CAP_AOA_TX);
      

      main.c::main()

      Once you have done the required modification on the embedded code, flash the LaunchPads and power-cycle them

  2. rtls_agent_cli settings:

    Verify if the passive's and master's capabilities listed by rtls_agent_cli match the settings you have previously done.

         WebSocket Server, ws://localhost:8766           | Waiting for data..
                                                         |
         Connected to 2 of 2 nodes                       |
                                                         |
                                                         |
         * COM12 @ COM12                                 |
             > XX:XX:XX:XX:XX:B4                         |
             > RTLS_MASTER                               |
                                                         |
         * COM14 @ COM14                                 |
             > XX:XX:XX:XX:XX:F0                         |
             > AOA_RX, RTLS_PASSIVE                      |
    
  1. Hardware settings:

    In this case, the passive device is not required. The BOOSTXL-AoA must be on the master device.

  2. Embedded Software settings:

    • rtls_passive: As previously said, the passive device is not required (no need to turn it on or to flash any image on it).

    • rtls_master: The master's RTLS Control module needs the capability to act as RTLS Master and AoA receiver. This capabilities are set modifying (if needed) the field rtlsCapab of the rtlsConfig structure (this must be done into the main.c file which is stored under the Startup folder). The capabilities we need for the lab are only RTLS_CAP_RTLS_MASTER and RTLS_CAP_AOA_RX.

      rtlsConfig.rtlsCapab = (rtlsCapabilities_e)(RTLS_CAP_RTLS_MASTER | RTLS_CAP_AOA_RX);
      

      main.c::main()

      Note: The python script configures the master device in order to select the appropriate IOs and switching pattern. The default parameters are as following:

        "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]
        }
      

      aoa_params used by default by the python script

      The radio core is responsible to handle the antenna toggling. The pattern followed and the IOs to use are given to the radio through overrides.

      Usually, the pyhton script send a command (rtlsUtil.aoa_set_params) to the Master in order to modify the content of the overrides. If you decide not using the python script, then you need to write the overrides yourself.

      Here we will focus on the overrides allowing the radio to handle the CTE. On the Master device, these overrides are stored in the table pOverridesCte in the file ble_user_config.c (under the folder iCallBLE of your project).

      Let's configure it:

            regOverride_t pOverridesCte[] = {
                0x00158000, // S2RCFG: Capture S2R from FrontEnd, on event (CM0 will arm)
                0x000E51D0, // After FRAC
                ((CTE_CONFIG << 16) | 0x8BB3), // Enable CTE capture
                ((CTE_OFFSET << 24) | ((CTE_SAMPLING_CONFIG_1MBPS | (CTE_SAMPLING_CONFIG_2MBPS << 4)) <<    16) | 0x0BC3), // Sampling rate, offset
                0xC0040341, // Pointer to antenna switching table in next entry
                (uint32_t) antSwitching, // Pointer to antenna switching table
                END_OVERRIDE };
      

      ble_user_config.c

      As you can see, we point to an antenna switching table. Let's declare it (in the same file, before the pOverridesCte table):

            #define NUM_ENTRIES  3
            #define SWITCH_TIME  2
      
            #define ANT1         (1<<28)
            #define ANT2         (1<<29)
            #define ANT3         (1<<30)
            #define IO_MASK      (ANT1 | ANT2 | ANT3)
      
            uint32_t antSwitching[] =
            {
              NUM_ENTRIES | (SWITCH_TIME << 8),
              IO_MASK,
              ANT1,
              ANT2,
              ANT3
            };
      

      ble_user_config.c for Master

      For curious people...

      The rtls_passive project also uses overrides to ask the radio to handle the CTE. The overrides used are exactly the same (obvious as the radio core is identical on the passive and the master devices). However the overrides are not stored in the same file. This is because the passive device does not use the BLE stack but the BLE Micro-Stack. On the passive device, the override list can be found in urfc.c:: pOverridesCommon (this file is stored under the Startup directory)

    • rtls_slave: Ensure the slave's RTLS Control module has the capability to act as a slave (RTLS_CAP_RTLS_SLAVE) and to send out the CTE (RTLS_CAP_AOA_TX).

      rtlsConfig.rtlsCapab = (rtlsCapabilities_e)(RTLS_CAP_RTLS_SLAVE | RTLS_CAP_AOA_TX);
      

      main.c::main()

      Once you have done the required modification on the embedded code, flash the LaunchPads and power-cycle them

  3. rtls_agent_cli settings:

    Only the Master is supposed to be connected to the computer. As a result, only the Master device is supposed to be detected by rtls_agent_cli. Verify if the master's capabilities listed by rtls_agent_cli match the settings you have previously done.

         WebSocket Server, ws://localhost:8766           | Waiting for data..
                                                         |
         Connected to 1 of 1 nodes                       |
                                                         |
                                                         |
         * COM12 @ COM12                                 |
             > XX:XX:XX:XX:XX:B4                         |
             > AOA_RX, RTLS_MASTER                       |
    
  4. GUI settings:

Now, test your setting

Same procedure as what we did in the RTLS intro

If AoA values are inaccurate...

Keep in mind that an office environment is not suitable for AoA evaluation. Many metallic and RF reflective surfaces are around you and provoke multipath. Try to find a more open environment.

AoA inaccuracies might also be caused by a bad antenna connection. Verify that the C51 capacitor of your LaunchPad is correctly positioned.

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

The out of box SimpleLink CC13X2-26X2 SDK 3.30 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. This logic is used both for rtls_passive and rtls_master:

  // Switch ant array
  if (gAoaReport.antConfig == antA1Config)
  {
    gAoaReport.antConfig = antA2Config;
    gAoaReport.antResult = antA2Result;

    // Setting the switch to 0 will use array 2 (when using BOOSTXL-AOA)
    PINCC26XX_setOutputValue(antArraySwitchIo, 0);
  }
  else if (gAoaReport.antConfig == antA2Config)
  {
    gAoaReport.antConfig = antA1Config;
    gAoaReport.antResult = antA1Result;

    // Setting the switch to 1 will use array 1 (when using BOOSTXL-AOA)
    PINCC26XX_setOutputValue(antArraySwitchIo, 1);
  }

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- Modify rtls_passive project

  1. Start from the rtls_passive_app_CC26x2R1_LAUNCHXL_tirtos_ccs project and rename it to rtls_passive_app_a1_CC26x2R1_LAUNCHXL_tirtos_ccs

  2. You could edit the files containing antenna array 2 dependencies to remove all of them. However these changes are a little annoying. So instead you can take advantage of the user configurable parameters (you can have a look to the BLE5-Stack User's Guide for the details). Add the following line into the file rtls_passive_app.opt (the file is stored in Tools/Defines):

    -DANT_CONFIG_1_ONLY
    

    rtls_passive_app.opt

2- Modify rtls_master project

  1. Start from the rtls_master_app_CC26x2R1_LAUNCHXL_tirtos_ccs project and rename it to rtls_master_app_a1_CC26x2R1_LAUNCHXL_tirtos_ccs

  2. You could edit the files containing antenna array 2 dependencies to remove all of them. However these changes are a little annoying. So instead you can take advantage of the user configurable parameters (you can have a look to the BLE5-Stack User's Guide for the details). Add the following line into the file rtls_master_app.opt (the file is stored in Tools/Defines):

    -DANT_CONFIG_1_ONLY
    

    rtls_master_app.opt

Now, test your program

Reuse the same procedure that was used in the RTLS intro (don't worry, the GUI is still compatible)

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 antenna toggling pattern is controlled by radio core, therefore the pattern and the pins are passed on to radio core through override list.

More information!

Even though the radio core does the antenna toggling, the application layer is still responsible to initialize the pins to the correct configuration and the array switch if the feature is required.

The pin initialization is taken care in AOA.c::AOA_initAntArray.

1- Modify rtls_passive's overrides

For rtls_passive, the override list can be found in urfc.c:: pOverridesCommon (this file is stored under the Startup directory).

Here are the setting that's related to antenna toggling patterns and pins.

#define NUM_ENTRIES  3
#define SWITCH_TIME  2

#define ANT1         (1<<28)
#define ANT2         (1<<29)
#define ANT3         (1<<30)
#define IO_MASK      (ANT1 | ANT2 | ANT3)

uint32_t antSwitching[] =
{
  NUM_ENTRIES | (SWITCH_TIME << 8),
  IO_MASK,
  ANT1,
  ANT2,
  ANT3
};

The following steps will guide you through this process:

  1. Change the NUM_ENTRIES to 2.

    //Number of IO value entries in the table.
    //If this is less than the number of IQ sampling slots,
    //the IO value entries are repeated in a circular manner
    #define NUM_ENTRIES  2
    
  2. Change the IO_MASK to the following:

    // Bit mask defining the DIOs used for antenna switching.
    // A 1 indicates that the corresponding DIO is used.
    #define IO_MASK      (ANT1 | ANT2)
    
  3. Change the antSwitching[] to the following:

    uint32_t antSwitching[] =
    {
     NUM_ENTRIES | (SWITCH_TIME << 8),
     IO_MASK,
     ANT1,
     ANT2
    };
    
  4. Comment out the following in ant_array1_config_boostxl_rev1v1.c as it will not be used.

       //comment out line 63~76
       {// v23
        .a = 1,
        .b = 2,
        .sign = 1,
        .offset = -5,
        .gain = 0.9,
       },
       {// v13
        .a = 0,
        .b = 2,
        .sign = 1,
        .offset = -20,
        .gain = 0.50,
       },
    

    ant_array1_config_boostxl_rev1v1.c

2- Modify rtls_master's overrides

On the master's side, the overrides can be modified using the python script (i.e. you don't need to modify the embedded code). The procedure is relatively similar to that described in Task 1. The difference is that this time we only have two antennas.

So, the only thing you have to do is to modify the following lines of your python script:

    "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": 2,
        "aoa_ant_array_switch": 27,
        "aoa_ant_array": [28, 29]
    }

aoa_params used to only activate two antennas

For rtls_master, the override list can be found in ble_user_config_stack.c and the overrides are declared into ble_user_config.c (both files are stored into iCallBLE folder).

You already know where the CTE related overrides are, as YOU wrote them...

In theory, you have the following code:

    #define NUM_ENTRIES  3
    #define SWITCH_TIME  2

    #define ANT1         (1<<28)
    #define ANT2         (1<<29)
    #define ANT3         (1<<30)
    #define IO_MASK      (ANT1 | ANT2 | ANT3)

    uint32_t antSwitching[] =
    {
      NUM_ENTRIES | (SWITCH_TIME << 8),
      IO_MASK,
      ANT1,
      ANT2,
      ANT3
    };

ble_user_config.c

  1. Change the NUM_ENTRIES to 2.

    //Number of IO value entries in the table.
    //If this is less than the number of IQ sampling slots,
    //the IO value entries are repeated in a circular manner
    #define NUM_ENTRIES  2
    
  2. Change the IO_MASK to the following:

    // Bit mask defining the DIOs used for antenna switching.
    // A 1 indicates that the corresponding DIO is used.
    #define IO_MASK      (ANT1 | ANT2)
    
  3. Change the antSwitching[] to the following:

    uint32_t antSwitching[] =
    {
     NUM_ENTRIES | (SWITCH_TIME << 8),
     IO_MASK,
     ANT1,
     ANT2
    };
    
  4. Comment out the following in ant_array1_config_boostxl_rev1v1.c as it will not be used.

       //comment out line 63~76
       {// v23
        .a = 1,
        .b = 2,
        .sign = 1,
        .offset = -5,
        .gain = 0.9,
       },
       {// v13
        .a = 0,
        .b = 2,
        .sign = 1,
        .offset = -20,
        .gain = 0.50,
       },
    

    ant_array1_config_boostxl_rev1v1.c

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.

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 and/or the rtls_master 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, in what format and how they are accessed.

  • For the Passive device

    I/Q is stored in the radio core RAM(RFC_CTE_RFE_RAM_DATA) 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_getRfIqSamples(RF_Handle rfHandle, RF_CmdHandle cmdHandle, RF_EventMask events)
      {
        uint8_t state = 0;
        uint8_t lastCapture;
        uint32_t *cteData;
        uint32_t *extCteData = NULL;
    
        if (events & RF_EventCmdDone)
        {
          lastCapture = HWREGB(RFC_CTE_LAST_CAPTURE);
          // check which RAM is capturing samples
          if ((lastCapture == RFC_CTE_CAPTURE_RAM_MCE) || (lastCapture == RFC_CTE_CAPTURE_RAM_RFE))
          {
            // wait while buffer is busy
            do
            {
              state = (lastCapture == RFC_CTE_CAPTURE_RAM_MCE)?HWREGB(RFC_CTE_MCE_RAM_STATE):HWREGB(  RFC_CTE_RFE_RAM_STATE);
            } while ((state == RFC_CTE_RAM_STATE_BUSY) || (state == RFC_CTE_RAM_STATE_DUAL_BUSY));
    
            // check if buffer is ready for reading
            if ((state == RFC_CTE_RAM_STATE_READY) || (state == RFC_CTE_RAM_STATE_DUAL_READY))
            {
              if (lastCapture == RFC_CTE_CAPTURE_RAM_MCE)
              {
                // get the MCE buffer address
                cteData = (uint32_t *)RFC_CTE_MCE_RAM_DATA;
                extCteData = (uint32_t *)RFC_CTE_RFE_RAM_DATA;
              }
              else
              {
                // get the RFE buffer address
                cteData = (uint32_t *)RFC_CTE_RFE_RAM_DATA;
              }
    
              // This will trigger the upper layer which polls samplesReady
              if (cteData != NULL && extCteData == NULL)
              {
                memcpy(gAoaReport.samples, cteData, AOA_RES_MAX_SIZE * sizeof(AoA_IQSample));
                gAoaReport.sampleState = SAMPLES_READY;
              }
    
              // release the RAM so it should be available for next CTE
              if (lastCapture == RFC_CTE_CAPTURE_RAM_MCE)
              {
                HWREGB(RFC_CTE_MCE_RAM_STATE) = RFC_CTE_RAM_STATE_EMPTY;
              }
              else
              {
                HWREGB(RFC_CTE_RFE_RAM_STATE) = RFC_CTE_RAM_STATE_EMPTY;
              }
            }
          }
        }
    
        // If we don't have samples at this point then they are not valid
        if (gAoaReport.sampleState != SAMPLES_READY)
        {
          gAoaReport.sampleState = SAMPLES_NOT_VALID;
        }
      }
    

    AOA.c

    In rtls_passive, the function AOA_getRfIqSamples is executed (more or less directly) by the function AOA_postProcess (which is itself called by RTLSCtrl_postProcessAoa, i.e. at the end of each connection event).

  • For the Master device

    For the master device, the BLE stack is responsible for reading the I/Q samples from the radio core. The tables containing the I/Q samples (or at least pointers on them) are sent back by the BLE Stack through messages stored in the ICall queue. The message containing I/Q samples are handled by the function RTLSMaster_processRtlsSrvMsg (in rtls_master.c). This leads to the execution of the function RTLSCtrl_aoaResultEvt (in rtls_ctrl.c) which in turn enqueues a message of type AOA_RESULTS_EVENT in the RTLS message-queue.

    Finally (and as it is done on the passive's side) the AOA_RESULTS_EVENT triggers the function AOA_postProcess. The master does not execute the function AOA_getRfIqSamples (the I/Q samples are part of the pEvt structure passed to AOA_postProcess).

I/Q samples resolution

I and Q samples only have 13 bits resolution even though they occupy 16 bits space in radio core RAM (this applies for both the passive and the master devices). 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. Of course, if you are not using the passive device, you can comment the corresponding line.

     {"com_port": "COM12", "baud_rate": 460800, "name": "CC26x2 Master"},
     {"com_port": "COM14", "baud_rate": 460800, "name": "CC26x2 Passive"},
    

    Set up the COM ports used

  2. 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 (it is what we are going to do in this lab). In the rtls_master readme file, it's mentioned that the connection interval should be larger than 300ms to accommodate outputting all the samples.

    • Increase the UART baudrate.

      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

      Note: The procedure is the same for both passive and master.

      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": "COM12", "baud_rate": 921600, "name": "CC26x2 Master"},
          {"com_port": "COM14", "baud_rate": 921600, "name": "CC26x2 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 #100
    

    Set up connection interval to be 500ms

    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.

  3. 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

  4. 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

  5. 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

  6. 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.

  7. 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 between 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.

For the passive:

  1. Change the NUM_ENTRIES to 1.

    //Number of IO value entries in the table.
    //If this is less than the number of IQ sampling slots,
    //the IO value entries are repeated in a circular manner
    #define NUM_ENTRIES  1
    
  2. Change the IO_MASK to the following:

    // Bit mask defining the DIOs used for antenna switching.
    // A 1 indicates that the corresponding DIO is used.
    #define IO_MASK      (ANT2)
    
  3. Change the antSwitching[] to the following:

    uint32_t antSwitching[] =
    {
     NUM_ENTRIES | (SWITCH_TIME << 8),
     IO_MASK,
     ANT2
    };
    

For the master: This can be done directly using the Python script:

        "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": 1,
            "aoa_ant_array_switch": 27,
            "aoa_ant_array": [28]
        }

aoa_params used to deactivate antenna switching

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.