Channel Sounding¶
This chapter serves as a guide to the Texas Instruments Channel Sounding feature including application implementation, drivers, and middle layers of the stack. Bluetooth 6.0 specification Channel Sounding extends the capabilities of Bluetooth-based ranging beyond traditional RSSI and direction finding (DF) methods, enabling more accurate and cost-effective ranging designs.
The guide will cover the main principles of the Channel Sounding process, the process of adding Channel Sounding to a project, and an out of the box example project available within the SimpleLink Low Power F3 SDK.
The Channel Sounding chapters will cover:
Channel Sounding overview, discussing the what and why (see Channel Sounding Overview)
Channel Sounding APIs (see BLE Stack API Reference)
Enable Channel Sounding Demonstration (see How to enable Channel Sounding example)
The CC23xx or CC27xx LaunchPad allows for quick and easy development of BLE applications, such as Channel Sounding. To run the Channel Sounding example, you will need two TI CC23xx or CC27xx.
Note: To run the CS demo, one must use CC27xx as car node in order to support distance calculation.
Channel Sounding Overview¶
What is Channel Sounding? Bluetooth Channel Sounding (CS), introduced in the Bluetooth 6.0 specification, enables high-accuracy distance measurement between two BLE devices using phase-based ranging (PBR). This technique estimates the distance by measuring the phase difference between a received unmodulated tone and the device’s local oscillator (LO) across multiple RF frequencies in the 2.4 GHz band. Both devices—the initiator and the reflector—exchange tones and record phase information. These phase differences, when plotted against frequency, yield a curve whose slope directly corresponds to the physical distance between devices. Advanced signal processing helps correct for multipath interference, enhancing precision even in complex radio environments.
Why is Channel Sounding Important? Channel Sounding delivers high-level distance accuracy without needing external infrastructure, making it ideal for real-time location systems (RTLS), car access, and secure proximity-based services. It also introduces built-in security mechanisms like random frequency hopping, Round Trip Time (RTT) validation, and attack detection metrics (NADM) to defend against spoofing or relay attacks. Importantly, it integrates efficiently with existing Bluetooth LE protocols, supporting low-power, low-cost, and scalable deployment across consumer, industrial, and automotive applications.
Two BLE supported devices are needed for Channel Sounding for this version of the SimpleLink Low Power F3 SDK.
Car Node
Key Node
The car node is the initiator device and the key node is the reflector device. The initiator is the device that is in control of the channel sounding configuration, synchronization, and begins each channel sounding subevent. Firstly, Mode 0 steps are sent from the initiator to the reflector. The Mode 0 steps synchronize the reflector to the initiators configuration and sets up the timing for the unmodulate tones, or subevents. Next, Mode 1, 2, or 3 steps can be completed. These steps include (RTT), (PBR) or a combination of both. Each step is an unmodulated tone sent between the initiator and the reflector device. The phase change or the time of flight of the tone is then used to calculate the distance, and/or detect spoofing, or man in the middle attacks.
How to enable Channel Sounding example¶
Channel sounding example works using CCS and Python:
CCS is used to flash one CC23xx or CC27xx with an initiator code and the second with one with a reflector code.
Python is used to control car node and plot results of CS procedure.
The following section will go through the SW flow behind TI’s CS demo.
Figure 139. Channel Sounding Example Project Software Flow¶
Prerequisites¶
Hardware:
2 x SimpleLink CC23xx or CC27xx LaunchPad Target
2 x SimpleLink™ LaunchPad™ XDS110 Debugger
2 x USB cable
(Optional) 1 x Battery Pack for Reflector
Software:
TI Code Composer Studio (CCS)
Prepare CCS and Python environment¶
1. For the initiator node, import <SDK>/examples/rtos/LP_EM_CC2745R10/ble/car_node and set the example to initiate a pairing request within Bond Manager. Procedure to do this is described below.
Flash the car_node project onto one of the two CC23xx or CC27xx devices and keep it connected to your PC. The COM port of the ble_device_car_node_with_distance.py script should be set to the COM port of the car node.
Figure 140. Configure Car Node to Initiate Pairing¶
Figure 141. Configure COM Port for Car Node in Python¶
For reflector node, import
<SDK>/examples/rtos/LP_EM_CC2745R10/ble/key_node. Flash the key_node project onto the second CC23xx or CC27xx device. The reflector does not need to remain connected to your PC. It only needs to be powered.
Note: Advertising data in the key node project is used by python. If it is changed in CCS, make sure to change it in python script too.
(Optional) To configure 2x2 antenna setup for both CC23xx or CC27xx:
Python:
Go to
<SDK>/tools/ble/ble_agent/examples/ble_device_car_node_with_distance.py. Change in cs_set_procedure_params and cs_set_procedure_params_repeat “aci” from 0 to 7 and “preferred_peer_antenna” from 0b0001 to 0b0011;To use other antenna configurations, refer to the ACI map below. Additionally, the preferred_peer_antenna bitmap will dictate the number of antennas on the peer (reflector).
ACI
Ant Config
ACI=0
1x1
ACI=1
2x1
ACI=2
3x1
ACI=3
4x1
ACI=4
1x2
ACI=5
1x3
ACI=6
1x4
ACI=7
2x2
Note: The antenna configurations above are oriented initiator x reflector.
CCS:
Go to Syscfg -> RF STACKS -> BLE -> Channel sounding configuration -> Number of Antennas. Select 2 antennas, and configure the Antenna Mux Bitmap 0xC6. For different antenna configurations, refer to the note within the Antenna Mux Bitmap section of SysConfig or check the example below.
Syscfg -> TI DRIVERS -> RCL -> RCL Observable -> Additional RF GPIO Signals. Add PBEGPO2 and PBEGPO3.
Figure 142. Configure PBEGPOs for Multiple Antennas¶
Antenna Mux Bitmap value example: In case of multiple antenna usage, the device needs to know how to use a specific antenna. Antenna Mux Bitmap value corresponds to the signal levels of 2 pins the device uses to select an antenna. This value is a translation of antenna control based on a truth table. Below is how to determine this value regarding hardware configuration for 4 antennas.
Hardware configuration:
Swith truth table:
PBEGPO3
PBEGPO2
Antenna On
0
0
Antenna 2
0
1
Antenna 3
1
0
Antenna 4
1
1
Antenna 1
Antenna Muxing Bitmap calculation
Antenna 4
Antenna 3
Antenna 2
Antenna 1
PBOGPO3
PBEGPO2
PBOGPO3
PBEGPO2
PBOGPO3
PBEGPO2
PBOGPO3
PBEGPO2
1
0
0
1
0
0
1
1
0x9
0x3
Antenna Muxing Bitmap = 0x93
4. (Optional) Enabling extended debug outputs additional data to the serial terminal. Extended debug output includes data about Mode 0 steps including RSSI, packetQuality, and measuredFreqOffset, the distance of each antenna path according to each algorithm (MuSIC and NN), local and remote phase data, local and remote Antenna permutation index which is ordered by channels, and more. To enable this, add the CS_PROCESS_EXT_RESULTS define to the predefines of the Car Node example project. See the image below:
Figure 143. Navigate to project properties¶
Figure 144. Navigate to predefines¶
Figure 145. Add Extended Debug predefine¶
Press ok, and continue.
Additionally, the python script needs to be altered to enable extended debug output on the serial terminal. To do this, set extended_results to true in the wait_distance function, the cs_proc_enable function, and the start_cs function. See the images below:
Figure 146. Add Extended Debug predefine¶
Figure 147. Add Extended Debug predefine¶
Figure 148. Add Extended Debug predefine¶
Install Python 3.10.11
Note: Python 3.10 should be installed at C:\Python310
During installation select “Add Environment Variable”, otherwise you can add it later, shown in the images below:
Figure 149. Open System Properties¶
Figure 150. Open System Properties¶
Figure 151. Edit User Environment Variables¶
Open a command line tool such as Windows Power Shell and install and activate virtual environment. In case your network is behind a proxy, use the proxy command shown.
1 cd <ble_agent folder (SDK/tools/ble/ble_agent)>
2 c:\Python310\python.exe -m pip install virtualenv [--proxy <www.proxy.com>]
3 c:\Python310\python.exe -m venv .venv
4 .venv\Scripts\activate
Setup external packages in case your network is behind a proxy, use –proxy
1 python -m pip install --upgrade pip [--proxy <www.proxy.com>]
2 pip install --upgrade wheel [--proxy <www.proxy.com>]
3 pip install -r requirements.txt [--proxy <www.proxy.com>]
How to run Channel Sounding example¶
Python¶
Execute Python Script in the previously configured environment using:
1 cd \simplelink_lowpower_f3_source_sdk_9_10_00_70_eng\tools\ble\ble_agent
2 .venv\Scripts\activate
3 cd examples
4 python ble_device_car_node_with_distance.py
The script will automatically initiate a connection between the car node and key node and start a CS procedure. See an example of the output below:
Figure 152. Distance Output in Terminal¶
Enabling Channel Sounding in the Application¶
Channel Sounding (CS) is a connection based ranging technology. In order to use CS, a connection must be established. The first step to enabling CS is to create a connection. To enable CS within an example project, it is recommended to continuously scan and advertise to easily create a connection.
After a connection is established, begin the process of enabling CS.
Enable the CS callbacks, and enable the CS event handler.
1bStatus_t ChannelSounding_start()
2{
3bStatus_t status = USUCCESS;
4
5// Register to command complete events
6status = BLEAppUtil_registerEventHandler(&csCmdCompleteEvtHandler);
7
8if( status == USUCCESS )
9{
10 // Register Channel Sounding Callbacks
11 status = BLEAppUtil_registerCsCB();
12}
13
14// Return status value
15return status;
16}
2. Channel Sounding Capabilities of both the local and remote device need to be communicated. The local device is the initiator device and the remote device is the reflector. See the code snippet below:
1bStatus_t Application_readLocalCsCapabilities(void)
2{
3 csCapabilities_t localCsCapabilities;
4
5 status = CS_ReadLocalSupportedCapabilities(&localCsCapabilities);
6
7 return status;
8}
1typedef struct
2{
3 uint16_t connHandle;
4} ChannelSounding_readRemoteCapabilitiesParams_t
1bStatus_t Application_readRemoteCsCapabilities(ChannelSounding_readRemoteCapabilitiesParams_t* pRemoteCapabilitiesParams)
2{
3 bStatus_t status = FAILURE;
4
5 if(pRemoteCapabilitiesParams != NULL)
6 {
7 status = CS_ReadRemoteSupportedCapabilities(&localCsCapabilities);
8 }
9
10 return status;
11}
Once the function CS_ReadRemoteSupportedCapabilities() is called, the local device sends a request to the remote device to read its capabilities. The remote device will respond to the local device with its capabilities. The Channel Sounding Capabilities can be read within the CS_READ_REMOTE_SUPPORTED_CAPABILITIES_COMPLETE_EVENT. Within this event, the capabilities will need to be populated into a structure. The data is received through the pMsgData pointer, used in the event handler. See an example of the capabilities structure below:
1typedef struct
2{
3 uint8_t optionalModes; //!< indicates which of the optional CS modes are supported
4 uint8_t rttCap; //!< indicate which of the time-of-flight accuracy requirements are met
5 uint8_t rttAAOnlyN; //!< Number of CS steps of single packet exchanges needed
6 uint8_t rttSoundingN; //!< Number of CS steps of single packet exchanges needed
7 uint8_t rttRandomPayloadN; //!< Num of CS steps of single packet exchange needed
8 uint16_t nadmSounding; //!< NADM Sounding Capability
9 uint16_t nadmRandomSeq; //!< NADM Random Sequence Capability
10 uint8_t optionalCsSyncPhy; //!< supported CS sync PHYs, bit mapped field
11 uint8_t numAntennas; //!< the number of antenna elements that are available for CS tone exchanges
12 uint8_t maxAntPath; //!< max number of antenna paths that are supported
13 uint8_t role; //!< initiator or reflector or both
14 uint8_t rfu0; //!< reserved for future use
15 uint8_t noFAE; //!< No FAE
16 uint8_t chSel3c; //!< channel selection 3c support
17 uint8_t csBasedRanging; //!< CS based ranging
18 uint8_t rfu1; //!< reserved for future use
19 uint8_t numConfig; //!< Number of CS configurations supported per conn
20 uint16_t maxProcedures; //!< Max num of CS procedures supported
21 uint8_t tSwCap; //!< Antenna switch time capability
22 uint16_t tIp1Cap; //!< tIP1 Capability
23 uint16_t tIp2Cap; //!< tTP2 Capability
24 uint16_t tFcsCap; //!< tFCS Capability
25 uint16_t tPmCsap; //!< tPM Capability
26 uint8_t snrTxCap; //!< Spec defines an additional byte for RFU
27} csCapabilities_t;
3. When local and remote capabilities are read, the next step is to enable CS security. This is done by calling the CS_SecurityEnable() function. Once the function is called, wait for the CS_SECURITY_ENABLE_COMPLETE_EVENT event to be received. The event will contain the status of the security enable request.
1typedef struct
2{
3 uint16_t connHandle; //!< Connection handle
4} CS_securityEnableCmdParams_t;
1bStatus_t Application_SecurityEnable(CS_securityEnableCmdParams_t* pSecurityEnableParams)
2{
3 bStatus_t status = FAILURE;
4
5 if(pSecurityEnableParams = NULL)
6 {
7 status = CS_SecurityEnable((CS_securityEnableCmdParams_t *) pCSsecurityEnableParams);
8 }
9
10 return status;
11}
4. No the default settings need to be configured. The CS_SetDefaultSettings function uses the CS_setDefaultSettingsCmdParams_t structure to set the default settings. These parameters include the connection handle, roleEnable flag, csSyncAntennaSelection, and maxTxPower.
1typedef struct
2{
3uint16_t connHandle; //!< Connection handle
4uint8_t roleEnable; //!< Role enable flag
5uint8_t csSyncAntennaSelection; //!< CS sync antenna selection
6int8_t maxTxPower; //!< Maximum TX power in dBm
7} CS_setDefaultSettingsCmdParams_t;
1bStatus_t Application_SetDefaultSettings(CS_setDefaultSettingsCmdParams_t* pDefaultSettings)
2{
3 bStatus_t status = FAILURE;
4
5 if(pDefaultSettings != NULL)
6 {
7 status = CS_SetDefaultSettings((CS_setDefaultSettingsCmdParams_t *) pDefaultSettings);
8 }
9
10 return status;
11}
5. Configure the CS configuration. The CS configuration outlines the parameters used by the local and remote devices when executing CS procedures. The CS_CreateConfig() function uses the CS_createConfigCmdParams_t structure to create the configuration. See the structure and the example function code below:
1typedef struct
2{
3uint16_t connHandle; //!< Connection handle
4uint8_t configID; //!< Configuration ID
5uint8_t createContext; //!< Create context flag
6uint8_t mainMode; //!< Main mode @ref CS_Mode
7uint8_t subMode; //!< Sub mode @ref CS_Mode
8uint8_t mainModeMinSteps; //!< Minimum steps for main mode
9uint8_t mainModeMaxSteps; //!< Maximum steps for main mode
10uint8_t mainModeRepetition; //!< Main mode repetition
11uint8_t modeZeroSteps; //!< Steps for mode zero
12uint8_t role; //!< Role @ref CS_Role
13uint8_t rttType; //!< RTT type @ref CS_RTT_Type
14uint8_t csSyncPhy; //!< CS sync PHY @ref CS_Sync_Phy_Supported
15csChm_t channelMap; //!< Channel map @ref csChm_t
16uint8_t chMRepetition; //!< Channel map repetition
17uint8_t chSel; //!< Channel selection algorithm to be used @ref CS_Chan_Sel_Alg
18uint8_t ch3cShape; //!< Channel 3C shape
19uint8_t ch3CJump; //!< Channel 3C jump
20} CS_createConfigCmdParams_t;
1bStatus_t Application_createCsConfiguration(CS_createConfigCmdParams_t* pCreateConfigParams)
2{
3 bStatus_t status = FAILURE;
4
5 if(pCreateConfigParams != NULL)
6 {
7 status = CS_CreateConfig((CS_createConfigCmdParams_t *) pCreateConfigParams);
8 }
9
10 return status;
11}
The CS_CreateConfig() function will trigger the CS_CONFIG_COMPLETE_EVENT event. The configuration parameters will be outputted within this event, and can be stored within a structure if desired.
Next, the procedure parameters are set. See the function and parameter structure in the code block below:
1typedef struct
2{
3uint16_t connHandle; //!< Connection handle
4uint8_t configID; //!< Configuration ID
5uint16_t maxProcedureDur; //!< Maximum procedure duration in 0.625 milliseconds
6uint16_t minProcedureInterval; //!< Minimum number of connection events between consecutive CS procedures
7uint16_t maxProcedureInterval; //!< Maximum number of connection events between consecutive CS procedures
8uint16_t maxProcedureCount; //!< Maximum number of CS procedures to be scheduled (0 - indefinite)
9uint32_t minSubEventLen; //!< Minimum SubEvent length in microseconds, range 1250us to 4s
10uint32_t maxSubEventLen; //!< Maximum SubEvent length in microseconds, range 1250us to 4s
11csACI_e aci; //!< Antenna Configuration Index @ref csACI_e
12uint8_t phy; //!< PHY @ref CS_Phy_Supported
13uint8_t txPwrDelta; //!< Tx Power Delta, in signed dB
14uint8_t preferredPeerAntenna; //!< Preferred peer antenna
15uint8_t snrCtrlI; //!< SNR Control Initiator
16uint8_t snrCtrlR; //!< SNR Control Reflector
17uint8_t enable; //!< Is procedure enabled @ref CS_Enable
18} CS_setProcedureParamsCmdParams_t;
1bStatus_t Application_SetCSProcedureParams(CS_setProcedureParamsCmdParams_t* pSetProcedureParams)
2{
3 bStatus_t status = FAILURE;
4
5 if(pSetProcedureParams != NULL)
6 {
7 status = CS_CreateConfig((CS_setProcedureParamsCmdParams_t *) pSetProcedureParams);
8 }
9
10 return status;
11}
After setting the procedure parameters, the Channel Sounding procedure can be enabled. See the functions and the structures below:
1typedef struct
2{
3uint16_t connHandle; //!< Connection handle
4uint8_t configID; //!< Configuration ID
5uint8_t enable; //!< Enable or disable the procedure @ref CS_Enable
6} CS_setProcedureEnableCmdParams_t;
1bStatus_t CS_ProcedureEnable(CS_setProcedureEnableCmdParams_t* pCsProcedureEnableParams)
2{
3bStatus_t status = FAILURE;
4
5if ( pCsProcedureEnableParams != NULL )
6{
7 status = CS_ProcedureEnable((CS_setProcedureEnableCmdParams_t *) pCsProcedureEnableParams);
8}
9
10return status;
11}
8. The CS_PROCEDURE_ENABLE_COMPLETE_EVENT will trigger when the CS procedure is complete. After this event is triggered, the CS_SUBEVENT_RESULT or CS_SUBEVENT_CONTINUE_RESULT event will be triggered. These events occur at the end of each subevent. After processing the subevent data, to prepare for post processing, the data can be sent to the BleCsRanging_estimatePbr function which will handle the (PBR) post processing. After the phase data is processed, the distance data can be post processed as well. The BLE CS Ranging Library includes distance post processing algorithms such as a Kalman filter, a moving average filter and a combination of both. Below is an example on how the phase data post processing and the distance data post processing is handled:
1BleCsRanging_Status_e BleCsRangingStatus; //Status for the BleCsRanging_estimatePbr function
2BleCsRanging_Tone_t* localResults; //Local Phase results from CS procedure
3BleCsRanging_Tone_t* remoteResults; //Remote Phase results from CS procedure
4
5if (gCsProcessDb->localRole == CS_ROLE_INITIATOR)
6{
7 localResults = gCsProcessDb->initPathsData;
8 remoteResults = gCsProcessDb->refPathsData;
9}
10else
11{
12 localResults = gCsProcessDb->refPathsData;
13 remoteResults = gCsProcessDb->initPathsData;
14}
15
16BleCsRangingStatus = BleCsRanging_estimatePbr(&results, localResults, remoteResults, &(gCsProcessDb->config));
17
18if (BleCsRangingStatus != BleCsRanging_Status_Success)
19{
20 status = FAILURE;
21}
22else
23{
24 // Get Current time
25 currTime = llGetCurrentTime();
26
27 // Initiate filtering for the first distance measurement.
28 // This should only be done before the first usage of the filtering
29 if (gCsProcessDb->filteringDb.initDone == FALSE)
30 {
31 // Init filters DB
32 gCsProcessDb->filteringDb.initDone = TRUE;
33 gCsProcessDb->filteringDb.lastTimeTicks = currTime;
34 gCsProcessDb->filteringDb.lastTime = ((float) currTime) / ((float) RAT_TICKS_IN_1S);
35 BleCsRanging_initKalmanFilter(&gCsProcessDb->filteringDb.kalmanFilter, results.distance , 0.1, 0.0, 1, gCsProcessDb->filteringDb.lastTime);
36 BleCsRanging_initMovingAverageFilter(&gCsProcessDb->filteringDb.movingAvgFilter, 4, 2);
37 }
38 else
39 {
40 // Add delta between current time and previous time difference to the filtering time
41 gCsProcessDb->filteringDb.lastTime += ((float) llTimeAbs(gCsProcessDb->filteringDb.lastTimeTicks, currTime)) / ((float) RAT_TICKS_IN_1S);
42
43 // Update last time measure to be the current time
44 gCsProcessDb->filteringDb.lastTimeTicks = currTime;
45 }
46
47 // Filter the raw results and get the final distance
48 results.distance = BleCsRanging_filterAll(&gCsProcessDb->filteringDb.movingAvgFilter, &gCsProcessDb->filteringDb.kalmanFilter,
49 BleCsRanging_FilterChain_AverageKalman, results.distance,
50 gCsProcessDb->filteringDb.lastTime);
51
52 // Copy the results, while converting floats to 32bits without losing the data after the decimal point
53 csResults->distance = (uint32_t) (results.distance * 100);
54 csResults->quality = (uint32_t) (results.quality * 100);
55 csResults->confidence = (uint32_t) (results.confidence * 100);
56}