TI CC2755R10 Bluetooth Channel Sounding: How to Measure Distance with BLE

For 15 years, BLE proximity meant squinting at RSSI and hoping. A device 2 meters away reads -65 dBm. So does one 8 meters away through a wall. So does the same device 2 meters away after you rotate it 30 degrees. The whole stack of “BLE ranging” products built on top of this was, charitably, a guess with a confidence interval the size of a room.
Bluetooth 6.0 Channel Sounding (CS) fixes this at the spec level, and the TI CC2755R10 is one of the first parts shipping with the hardware to do it. If you’ve got a LaunchPad on your desk and the SimpleLink CC27xx SDK installed, this is the code-level walkthrough for getting from “CS procedure configured” to “distance in meters” on your serial console. One disclaimer up front: SDK identifiers below match the current CC27xx CS surface at time of writing, but TI iterates fast. Verify names against your installed headers before you copy anything wholesale.
CS Fundamentals: PBR vs RTT in 200 Words
CS gives you two ranging methods, and a production-grade distance estimate uses both.
Round-Trip Time (RTT) is what it sounds like: the initiator sends a CS_SYNC packet, the reflector echoes it, and the controller timestamps both ends with sub-nanosecond resolution. Divide by 2, multiply by c, and you’ve got distance. RTT is coarse (roughly 30 cm resolution, bounded by clock granularity) but it’s unambiguous and largely immune to multipath. The first reflection wins.
Phase-Based Ranging (PBR) is where the precision lives. Initiator and reflector exchange continuous-wave tones across many channels. The controller samples the tone as IQ data. Phase difference between transmitted and received tone varies linearly with frequency, and the slope of that line gives you distance:
d = -(c / 4π) · dφ/dfPBR is precise to centimeters in a clean environment. The catch: phase wraps every half-wavelength, so at 2.4 GHz channel spacing, your PBR answer is ambiguous every ~37.5 cm. You don’t know which wrap you’re in. RTT tells you.
Initiator Reflector
|---- CS_SYNC (RTT t0) -------->|
|<--- CS_SYNC_RSP (RTT t1) -----|
|---- Tone @ f_k -------------->| (capture IQ)
|<--- Tone @ f_k ---------------| (capture IQ)
| ... repeat over N channels ...TI Channel Sounding: CC2755R10 Architecture & SDK Surface
CS sits across the BLE host and controller. Your application calls GAP-layer CS APIs, the host translates to HCI CS commands, the controller schedules the procedure, and the RF core drives the tone exchanges and captures IQ samples. You don’t write the tone generation. You write the configuration and the post-processing.
The SimpleLink SDK exposes:
GAP_CS_ConfigureProcedure()andGAP_CS_StartProcedure()for procedure setup and kickoff- A
CS_SubeventResults_tevent delivered per subevent, containing per-step IQ samples and RTT timestamps - Capability negotiation APIs that let you check what the peer supports before you assume Mode-3
The critical config knob is CS Mode:
- Mode-0: calibration steps, no ranging payload. Required at the start of every procedure to characterize hardware bias.
- Mode-1: RTT only.
- Mode-2: PBR only.
- Mode-3: mixed RTT + PBR per step. This is what you want for production.
Channel selection matters too. The spec defines 72 non-overlapping 1 MHz channels in the 2.4 GHz band, and CS uses a randomized hopping sequence (driven by the CS_SYNC access address) for both ranging diversity and resistance to relay attacks. More channels means better PBR resolution and stronger outlier rejection, but the procedure takes longer and burns more energy. 40-50 channels is a reasonable starting point.
Code Walkthrough: Initiator Setup
Here’s the configuration shape. Treat it as illustrative, not buildable:
CS_Config_t cfg = {
.role = CS_ROLE_INITIATOR,
.main_mode = CS_MODE_3, // RTT + PBR
.sub_mode = CS_MODE_1,
.min_main_mode_steps = 2,
.max_main_mode_steps = 5,
.rtt_type = CS_RTT_AA_ONLY,
.cs_sync_phy = CS_PHY_1M,
.channel_map = { /* 72 non-overlapping 1MHz ch */ },
};
GAP_CS_ConfigureProcedure(connHandle, &cfg);
GAP_CS_StartProcedure(connHandle);A few notes on the knobs that bite:
rtt_type = CS_RTT_AA_ONLY uses just the access address for timestamping. It’s the fastest and lowest-energy RTT mode. If you need tighter RTT precision (and you’re willing to spend airtime), CS_RTT_32B_SOUNDING or the random sequence variants give the controller more bits to correlate against.
cs_sync_phy = CS_PHY_1M is the safe default. 2M PHY is supported and shortens procedures, but check peer capability first.
On security: the access address for CS_SYNC packets comes from procedure-specific keys, randomized per procedure. That’s what makes CS resistant to the classic BLE relay attack, where an attacker rebroadcasts your phone’s beacons to fool a car door into thinking you’re standing next to it. If you’re building a smart lock or keyless entry product, that’s the whole reason you’re here. Don’t disable any of the randomization options for “debugging convenience” and then forget to turn them back on.
Capturing & Interpreting IQ Data
Most of your engineering effort lands in the CS subevent callback. The controller hands you a CS_SubeventResults_t with per-step IQ samples and RTT measurements. You filter, unwrap, fit, and fuse.
void onCsSubeventComplete(CS_SubeventResults_t *r) {
for (uint8_t k = 0; k < r->num_steps; k++) {
CS_Step_t *s = &r->steps[k];
// PBR: capture phase from tone IQ
if (s->mode == CS_MODE_2 || s->mode == CS_MODE_3) {
if (s->tone_quality_indicator >= NQI_THRESHOLD) {
float phase = atan2f(s->tone_iq.q, s->tone_iq.i);
phase_per_channel[s->channel_idx] = phase;
channel_valid[s->channel_idx] = true;
}
}
// RTT: capture time-of-arrival / time-of-departure delta
if (s->mode == CS_MODE_1 || s->mode == CS_MODE_3) {
rtt_samples[k] = s->packet_quality.toa_tod_diff_ns;
}
}
estimate_distance();
}Three things to handle carefully:
NQI filtering. Each tone comes with a Nominal Quality Indicator. Tones below your NQI threshold are noise-dominated and will skew the linear fit. Drop them.
Phase unwrap. Raw atan2 outputs wrap at ±π. Walk the channel index in order and add 2π whenever you see a jump greater than π between adjacent channels. This is textbook DSP, but it’s where most homemade implementations break first.
Reflector phase correction. Both initiator and reflector report their phase measurements. Subtract them to cancel local oscillator phase offset. Skip this and your distance will swim around with crystal drift.
Plotted against frequency, your unwrapped phase should look like this:
phase (rad)
π | *
| *
0 |------*-----------------
| *
-π | *
+----+----+----+----+---> freq (MHz)
2402 2422 2442 2462A clean straight line means you’re in good shape. A scatter plot means multipath, low SNR, or both.
Distance Estimation and CC27xx Ranging Math
PBR distance falls out of a linear fit on the unwrapped phase:
// phase = m * freq + b
float slope = linear_regression(freqs_hz, unwrapped_phase, n_valid);
float d_pbr = -(SPEED_OF_LIGHT / (4.0f * PI)) * slope;RTT distance is the easy part:
float d_rtt = (SPEED_OF_LIGHT * rtt_median_ns * 1e-9f) / 2.0f;Now fuse them. Use RTT to resolve which PBR wrap you’re in:
float wrap = SPEED_OF_LIGHT / (2.0f * channel_spacing_hz);
int n = roundf((d_rtt - d_pbr) / wrap);
float d_final_raw = d_pbr + n * wrap;Then smooth across procedures. A 5-sample median followed by a light IIR (α ≈ 0.3) handles both outliers and tracks moving targets without too much lag. Reject the whole procedure if you have fewer than ~20 NQI-passing tones, the result won’t be trustworthy.
[IQ tones] -> [unwrap] -> [linear fit] ----> d_pbr
\
[CS_SYNC ToA/ToD] -> [median] -> d_rtt ---> [wrap resolver] -> [median/IIR] -> d_finalWhere Reality Bites
On the bench, line-of-sight, you’ll see sub-meter accuracy without much effort. Then you put it in a real building and things get harder.
Multipath. Indoor reflections add phase contributions from paths that aren’t the direct one, and the bias they introduce is the dominant error source in any real deployment. Channel diversity helps: averaging across 40+ channels washes out frequency-selective fading, since each reflection path has a different frequency response and the constructive/destructive interference shifts with channel. Diversity alone won’t eliminate the bias from a strong specular reflector close to the direct path. The deepest fix is per-channel outlier rejection on the residuals from your linear fit: throw out the channels where the measured phase deviates most from the best-fit line, then refit.
Antenna phase center variation. The “electrical” position of your antenna moves with orientation. Expect ~10 cm of bias from this alone unless you calibrate per-product.
Temperature drift. RF front-end group delay changes with die temperature. Mode-0 calibration exists for exactly this. Don’t skip it.
Clock asymmetry on the reflector contributes a fixed RTT bias. Run RTT in both directions (initiator-to-reflector and reflector-to-initiator) and average the two. The asymmetry cancels.
Porting This to Other Silicon
The math in the last two sections ports across vendors. Nordic’s nRF54L and Silicon Labs’ xG24 expose conceptually similar IQ data through their own SDKs, and if you’re evaluating CS-capable silicon for an asset tracking, smart lock, or indoor positioning product, you’ll find the post-processing pipeline is roughly 80% of the firmware effort regardless of which part you pick. If you’re integrating CS-derived locations into a wider device fleet, our BLE asset tracking implementation guide covers how ranging fits alongside Hubble’s terrestrial and satellite backhaul.
Next up on this track: implementing the reflector role on CC27xx (it’s not just “the initiator with a flag flipped”), and multi-antenna fusion of CS distance with AoA bearing for full 2D position from a single anchor.
Hubble Network turns BLE-derived ranges into device locations at scale, with terrestrial and satellite backhaul handling the uplink wherever your trackers end up. See how it works →