Bluetooth Channel Sounding es una nueva función de Bluetooth Low Energy (BLE) que permite a los dispositivos medir la distancia de forma precisa.
Esta tecnología utiliza dos métodos principales: Phase-Based Ranging (PBR), que calcula la distancia analizando los cambios de fase de la señal en diferentes frecuencias, y Round-Trip Time (RTT), que mide el tiempo que tarda una señal en ir y regresar entre dos dispositivos. La combinación de ambos permite mejorar la precisión y agregar seguridad contra ataques de retransmisión. Puede alcanzar mediciones con precisión de nivel centimétrico en condiciones adecuadas.

Iniciador (Initiator)
En Bluetooth Channel Sounding, los iniciadores contienen el algoritmo de nivel de aplicación que convierte los datos brutos provenientes de la capa física (PHY) y la pila de Bluetooth en una medición de distancia. El iniciador de Channel Sounding de Nordic Semiconductor, que incluye una muestra de solicitante de medición de distancia, contiene un algoritmo IFFT de código abierto y gratuito, adecuado para casos de uso de medición de distancia sencillos.
Para sistemas de medición de distancias más avanzados, Nordic Semiconductor colabora con empresas asociadas que ofrecen algoritmos a medida para su uso en aplicaciones como la trilateración para sistemas de posicionamiento en interiores.
Reflector
Los Channel Sounding Reflectors responden al intercambio reflejando los tonos enviados por el iniciador. El Channel Sounding Reflector con respondedor de alcance de Nordic Semiconductors cuenta con la certificación Bluetooth DN a partir de la versión 3.0.1 del SDK de nRF Connect. Esto significa que puede compilar este ejemplo hoy mismo e interactuar con cualquier Channel Sounding Initiator que cumpla con la especificación.
La mayoría de los dispositivos integrados que se conectan a un teléfono móvil desempeñarán el papel de reflector en esta relación si se incluye la función de sondeo de canal Bluetooth.
Aplicaciones con Bluetooth Channel Sounding
Bluetooth Channel Sounding está pensado para aplicaciones como localización de dispositivos, rastreadores inteligentes, llaves digitales, control de acceso y sistemas IoT, donde conocer la distancia exacta entre dispositivos es importante.
- Entrada de acceso para vehículos: Una de las principales aplicaciones de Channel Sounding es el acceso remoto sin llave a un vehículo. Se utiliza para la detección zonal mediante medición de distancia para autenticar a los usuarios que se aproximan y desbloquear el vehículo. Utiliza la tecnología de medición de distancia Bluetooth a través de la conexión establecida entre los canales de datos (en este caso, el mando a distancia o el teléfono) y los puntos de anclaje del coche.

- Geolocalización: La geolocalización se utiliza para automatizar y mejorar la seguridad restringiendo el acceso a áreas específicas predefinidas y rastreando los dispositivos inteligentes y sus movimientos. El análisis de canales puede ayudar a optimizar las aplicaciones de geolocalización y mejorar el rendimiento y la fiabilidad de las redes.
- Cerraduras inteligentes: Las cerraduras inteligentes se pueden controlar de forma remota mediante un dispositivo inteligente y solo permiten el acceso cuando el dispositivo autorizado se encuentra dentro de un rango de acceso específico. La monitorización de canales (Channel Sounding) puede utilizarse para mejorar la precisión y la fiabilidad de estos dispositivos, y contribuir a su mayor seguridad.
- Acceso sin llaves: El sistema Channel Sounding garantiza que solo los usuarios y dispositivos autorizados puedan acceder a los sistemas (como vehículos) si se encuentran dentro de un rango de proximidad predefinido a dicho sistema.
- Rastreo de mascotas: El seguimiento de mascotas es otra aplicación en la que se puede utilizar el sondeo de canales para mejorar la precisión del seguimiento y la localización, lo que ayuda a los dueños a mantenerse informados sobre el paradero de su mascota.
- Seguimiento de artículos: Al igual que otras soluciones de localización, Channel Sounding ayuda a mejorar la precisión y la fiabilidad de los dispositivos que ayudan a rastrear y localizar objetos.

Hardware
La función de bluetooth channel sounding esta disponible en dispositivos con bluetooth Core 6.0.
En el mercado existen diferentes chips de la serie nRF54L de Nordic semiconductor, incluyendo los SoC nRF54LM20A, nRF54L15 y nRF54L10. Algunas placas de desarrollo que puedes utilizar para implementar este proyecto son nRF54LM20 DK, nRF54LV10 DK, nRF54LC10 DK, nRF54L15 DK, Seeed Studio Xiao nRFLM20A Sense.
Seeed Studio XIAO nRF54LM20A Sense – Bluetooth Channel Sounding
El siguiente proyecto tiene como objetivo medir la distancia entre dos placas Xiao nRFLM20A utilizando la función de sondeo de canales de bluetooth Core 6.0. Las mediciones son consistentes en exteriores, mientras que en interiores las paredes y objetos cercanos pueden generar reflexión y alterar la mediciones.
Para sistemas de medición de distancias más avanzados, Nordic Semiconductor colabora con empresas asociadas que ofrecen algoritmos a medida para su uso en aplicaciones como la trilateración para sistemas de posicionamiento en interiores.
Además en placas como la nRF54LM20 DK integra una función de detección con múltiples antenas que puede mejorar la medición. Esta función no esta disponible en la Xiao nRF54LM20A Sense.
Materiales
- 2x – XIAO nRF54LM20A Sense
- 2x – Baterías LiPo de 3.7V
- 1x – OLED SSD1306 de 128×64 pixeles
El código para programar la aplicación de Bluetooth Channel Sounding esta disponible en los ejemplos del SDK de Nordic. El codigo esta pensado para los Kids de desarrollo de la serie nRF54L de Nordic semiconductor, incluyendo nRF54LV10 DK, nRF54LM20 DK, nRF54L15 DK, por lo que hace falta agregar las configuraciones para la placa Xiao nRF54LM20A.
Se modificara el código para ajustar las mediciones mediante el método de regresión lineal debido a las diferencia de diseño de las antenas y la placa de desarrollo. Finalmente se agregara el código para visualizar las mediciones en la pantalla OLED.
Nota: Si no tienes instalado el SDK de Nordic en VS Code revisa el siguiente enlace:
Código Iniciador
Carga el proyecto de ejemplo desde el SDK de Nordic.
- Selecciona el SDK de Nordic.
- Crea una nueva aplicación.
- Selecciona «Copy a sample«
- Escribe «Sounding» en la barra del buscador.
- Selecciona el ejemplo «Bluetooth LE Channel Sounding Initiator with RREQ«.
- Selecciona la carpeta donde desees guardar el proyecto.
Agrega la configuración para la placa Xiao nRF54LM20A Sense y modifica el código para agregar la pantalla OLED SSD1306.
1 .- Iniciator – main.c
- Modifica el código del archivo: main.c
2.- Iniciator – prj.conf
- Modifica la configuración del archivo: prj.conf
3.- Iniciator – xiao_nrf54lm20a_nrf54lm20a_cpuapp.overlay
- Agrega un nuevo archivo en \boards
- Nombra el archivo como: xiao_nrf54lm20a_nrf54lm20a_cpuapp.overlay
- Agrega el código de la configuración.
3.1.- Iniciator – xiao_nrf54lm20a_nrf54lm20a_cpuapp.overlay
- Agrega un nuevo archivo en \boards
- Nombra el archivo como: xiao_nrf54lm20a_nrf54lm20a_cpuapp.overlay
- Agrega el código de la configuración.
Iniciator – main.c
- Modifica el código del archivo: main.c
/*
* Copyright (c) 2024 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
*/
/** @file
* @brief Channel Sounding Initiator with Ranging Requestor sample
*/
#include "math.h"
#include "stdlib.h"
#include "zephyr/kernel.h"
#include "zephyr/types.h"
#include "zephyr/sys/byteorder.h"
#include "zephyr/sys/reboot.h"
#include "zephyr/device.h"
#include "zephyr/drivers/display.h"
#include "zephyr/display/cfb.h"
#include "zephyr/sys/printk.h"
#include "zephyr/bluetooth/cs.h"
#include "zephyr/bluetooth/gatt.h"
#include "zephyr/bluetooth/conn.h"
#include "bluetooth/scan.h"
#include "bluetooth/services/ras.h"
#include "bluetooth/gatt_dm.h"
#include "bluetooth/cs_de.h"
#include "dk_buttons_and_leds.h"
#include "zephyr/logging/log.h"
LOG_MODULE_REGISTER(app_main, LOG_LEVEL_INF);
#define CON_STATUS_LED DK_LED1
#define CS_CONFIG_ID 0
#if defined(CONFIG_SAMPLE_RAS_INITIATOR_STEP_MODE_2_SUB_MODE_1)
#define CS_CONFIG_MODE BT_CONN_LE_CS_MAIN_MODE_2_SUB_MODE_1
#elif defined(CONFIG_SAMPLE_RAS_INITIATOR_STEP_MODE_2)
#define CS_CONFIG_MODE BT_CONN_LE_CS_MAIN_MODE_2_NO_SUB_MODE
#elif defined(CONFIG_SAMPLE_RAS_INITIATOR_STEP_MODE_1)
#define CS_CONFIG_MODE BT_CONN_LE_CS_MAIN_MODE_1_NO_SUB_MODE
#elif defined(CONFIG_SAMPLE_RAS_INITIATOR_STEP_MODE_3)
#define CS_CONFIG_MODE BT_CONN_LE_CS_MAIN_MODE_3_NO_SUB_MODE
#else
BUILD_ASSERT(false, "Invalid ranging mode");
#endif
#define NUM_MODE_0_STEPS 3
#define PROCEDURE_COUNTER_NONE (0)
#define DE_SLIDING_WINDOW_SIZE (9)
#define MAX_AP (CONFIG_BT_RAS_MAX_ANTENNA_PATHS)
#define LOCAL_PROCEDURE_MEM \
((BT_RAS_MAX_STEPS_PER_PROCEDURE * sizeof(struct bt_le_cs_subevent_step)) + \
(BT_RAS_MAX_STEPS_PER_PROCEDURE * BT_RAS_MAX_STEP_DATA_LEN))
#define CHANNEL_INDEX_OFFSET (2)
#define TONE_QI_OK_TONE_COUNT_THRESHOLD (65)
/* Mediciones sin regresión lineal
* Prueba con estos parámetros para conocer
* el offset de calibración de tu hardware.
*/
/*
#define PHASE_SLOPE_SCALE_FACTOR (1.0f)
#define PHASE_SLOPE_OFFSET_M (0.0f)
#define IFFT_SCALE_FACTOR (1.0f)
#define IFFT_OFFSET_M (0.0f)
*/
/*
* Regresión lineal con placas Xiao nRF54LM20A Sense
* Se tomaron 10 mediciones de distancias aproximadas de 0.5 a 5 metros,
* y se calculó la regresión lineal.
* Se puede mejorar con más mediciones y un rango más amplio de distancias reales precisas.
*/
#define IFFT_SCALE_FACTOR 0.97783f
#define IFFT_OFFSET_M 0.60938f
#define PHASE_SLOPE_SCALE_FACTOR 0.96375f
#define PHASE_SLOPE_OFFSET_M 0.64738f
static K_SEM_DEFINE(sem_remote_capabilities_obtained, 0, 1);
static K_SEM_DEFINE(sem_config_created, 0, 1);
static K_SEM_DEFINE(sem_cs_security_enabled, 0, 1);
static K_SEM_DEFINE(sem_connected, 0, 1);
static K_SEM_DEFINE(sem_discovery_done, 0, 1);
static K_SEM_DEFINE(sem_mtu_exchange_done, 0, 1);
static K_SEM_DEFINE(sem_security, 0, 1);
static K_SEM_DEFINE(sem_ras_features, 0, 1);
static K_SEM_DEFINE(sem_local_steps, 1, 1);
static K_SEM_DEFINE(sem_distance_estimate_updated, 0, 1);
static K_MUTEX_DEFINE(distance_estimate_buffer_mutex);
#define OLED_NODE DT_NODELABEL(ssd1306_128x64)
static const struct device *oled = DEVICE_DT_GET(OLED_NODE);
static int oled_init(void)
{
if (!device_is_ready(oled)) {
LOG_ERR("SSD1306 OLED no está listo");
return -ENODEV;
}
display_set_pixel_format(oled, PIXEL_FORMAT_MONO10);
cfb_framebuffer_init(oled);
cfb_framebuffer_clear(oled, true);
/* Fuente grande para el valor principal */
cfb_framebuffer_set_font(oled, 1);
cfb_print(oled, "--.--m", 0, 5);
/* Fuente pequeña para P y R */
cfb_framebuffer_set_font(oled, 0);
cfb_print(oled, "P:--.--m", 0, 33);
cfb_print(oled, "R:--.--m", 0, 48);
cfb_framebuffer_finalize(oled);
return 0;
}
static void oled_show_distance(const cs_de_dist_estimates_t *distance)
{
float main_distance = distance->ifft;
char main_str[16];
char phase_str[12];
char rtt_str[12];
char line[16];
uint8_t font_width;
uint8_t font_height;
if (isfinite(main_distance)) {
snprintk(main_str, sizeof(main_str), "%.2fm", (double)main_distance);
} else {
snprintk(main_str, sizeof(main_str), "---m");
}
/* P conserva el significado del código anterior: IFFT */
if (isfinite(distance->phase_slope)) {
snprintk(phase_str, sizeof(phase_str), "%.2fm", (double)distance->phase_slope);
} else {
snprintk(phase_str, sizeof(phase_str), "--");
}
/* R conserva el significado del código anterior: RTT */
if (isfinite(distance->rtt)) {
snprintk(rtt_str, sizeof(rtt_str), "%.2fm", (double)distance->rtt);
} else {
snprintk(rtt_str, sizeof(rtt_str), "--");
}
cfb_framebuffer_clear(oled, false);
cfb_framebuffer_set_font(oled, 1);
cfb_get_font_size(oled, 1, &font_width, &font_height);
size_t main_len = strlen(main_str);
int main_width = (int)main_len * font_width;
int main_x = (128 - main_width) / 2;
if (main_x < 0) { main_x = 0;} cfb_print(oled, main_str, main_x, 5); cfb_framebuffer_set_font(oled, 0); snprintk(line, sizeof(line), "P:%s", phase_str); cfb_print(oled, line, 0, 33); snprintk(line, sizeof(line), "R:%s", rtt_str); cfb_print(oled, line, 0, 48); cfb_framebuffer_finalize(oled); } static struct bt_conn *connection; NET_BUF_SIMPLE_DEFINE_STATIC(latest_local_steps, LOCAL_PROCEDURE_MEM); NET_BUF_SIMPLE_DEFINE_STATIC(latest_peer_steps, BT_RAS_PROCEDURE_MEM); static int32_t most_recent_local_ranging_counter = PROCEDURE_COUNTER_NONE; static int32_t dropped_ranging_counter = PROCEDURE_COUNTER_NONE; static uint32_t ras_feature_bits; struct distance_estimate_buffer { cs_de_dist_estimates_t estimates[DE_SLIDING_WINDOW_SIZE]; uint8_t num_valid; uint8_t index; }; static struct distance_estimate_buffer distance_estimate_buffers[MAX_AP]; static struct bt_conn_le_cs_config cs_config; static uint16_t m_n_iqs[CONFIG_BT_RAS_MAX_ANTENNA_PATHS][CS_DE_NUM_CHANNELS]; static cs_de_report_t m_cs_de_report; static void store_distance_estimates_in_buffer(cs_de_dist_estimates_t *p_estimates, struct distance_estimate_buffer *buffer) { int lock_state = k_mutex_lock(&distance_estimate_buffer_mutex, K_FOREVER); __ASSERT_NO_MSG(lock_state == 0); memcpy(&buffer->estimates[buffer->index], p_estimates, sizeof(cs_de_dist_estimates_t));
buffer->index = (buffer->index + 1) % DE_SLIDING_WINDOW_SIZE;
if (buffer->num_valid < DE_SLIDING_WINDOW_SIZE) { buffer->num_valid++;
}
k_mutex_unlock(&distance_estimate_buffer_mutex);
}
static int float_cmp(const void *a, const void *b)
{
float fa = *(const float *)a;
float fb = *(const float *)b;
return (fa > fb) - (fa < fb);
}
static float median_inplace(int count, float *values)
{
if (count == 0) {
return NAN;
}
qsort(values, count, sizeof(float), float_cmp);
if (count % 2 == 0) {
return (values[count/2] + values[count/2 - 1]) / 2;
} else {
return values[count/2];
}
}
/* Offset de calibración de hardware a una distancia ya calculada.
* Si el valor de entrada no es finito (NAN, por falta de datos válidos en la ventana),
* se devuelve tal cual para no generar un número falso.
*/
static float apply_calibration(float raw_distance, float scale, float offset_m)
{
if (!isfinite(raw_distance)) {
return raw_distance;
}
return (raw_distance * scale) - offset_m;
}
static cs_de_dist_estimates_t get_distance(uint8_t ap)
{
cs_de_dist_estimates_t averaged_result = {};
uint8_t num_ifft = 0;
uint8_t num_phase_slope = 0;
uint8_t num_rtt = 0;
static float temp_ifft[DE_SLIDING_WINDOW_SIZE];
static float temp_phase_slope[DE_SLIDING_WINDOW_SIZE];
static float temp_rtt[DE_SLIDING_WINDOW_SIZE];
struct distance_estimate_buffer *buffer = &distance_estimate_buffers[ap];
int lock_state = k_mutex_lock(&distance_estimate_buffer_mutex, K_FOREVER);
__ASSERT_NO_MSG(lock_state == 0);
for (uint8_t i = 0; i < buffer->num_valid; i++) {
if (isfinite(buffer->estimates[i].ifft)) {
temp_ifft[num_ifft] = buffer->estimates[i].ifft;
num_ifft++;
}
if (isfinite(buffer->estimates[i].phase_slope)) {
temp_phase_slope[num_phase_slope] = buffer->estimates[i].phase_slope;
num_phase_slope++;
}
if (isfinite(buffer->estimates[i].rtt)) {
temp_rtt[num_rtt] = buffer->estimates[i].rtt;
num_rtt++;
}
}
k_mutex_unlock(&distance_estimate_buffer_mutex);
averaged_result.ifft = apply_calibration(median_inplace(num_ifft, temp_ifft),
IFFT_SCALE_FACTOR, IFFT_OFFSET_M);
averaged_result.phase_slope = apply_calibration(median_inplace(num_phase_slope, temp_phase_slope),
PHASE_SLOPE_SCALE_FACTOR, PHASE_SLOPE_OFFSET_M);
averaged_result.rtt = median_inplace(num_rtt, temp_rtt);
return averaged_result;
}
static bool m_is_tone_quality_ok(uint16_t num_iqs[CS_DE_NUM_CHANNELS], uint8_t channel_map[10])
{
uint8_t ok_tones_count = 0;
for (uint8_t i = 0; i < CS_DE_NUM_CHANNELS; ++i) { if (BT_LE_CS_CHANNEL_BIT_GET(channel_map, i + CHANNEL_INDEX_OFFSET) && num_iqs[i] >= 1) {
ok_tones_count += 1;
}
}
LOG_INF("Tones OK: %u",
ok_tones_count);
return (ok_tones_count >= TONE_QI_OK_TONE_COUNT_THRESHOLD);
}
static void cumulate_mean(float *avg, float new_value, uint16_t *N)
{
float a = 1.0f / (*N);
float b = 1.0f - a;
*avg = a * new_value + b * (*avg);
}
static void extract_pcts(cs_de_report_t *p_report, uint8_t channel_index,
uint8_t antenna_permutation_index,
struct bt_hci_le_cs_step_data_tone_info *local_tone_info,
struct bt_hci_le_cs_step_data_tone_info *remote_tone_info)
{
for (uint8_t tone_index = 0; tone_index < p_report->n_ap; tone_index++) {
int antenna_path = bt_le_cs_get_antenna_path(p_report->n_ap,
antenna_permutation_index, tone_index);
if (antenna_path < 0) { LOG_WRN("Invalid antenna path."); return; } if (local_tone_info[tone_index].quality_indicator != BT_HCI_LE_CS_TONE_QUALITY_HIGH || remote_tone_info[tone_index].quality_indicator != BT_HCI_LE_CS_TONE_QUALITY_HIGH) { return; } struct bt_le_cs_iq_sample local_iq = bt_le_cs_parse_pct(local_tone_info[tone_index].phase_correction_term); struct bt_le_cs_iq_sample remote_iq = bt_le_cs_parse_pct(remote_tone_info[tone_index].phase_correction_term); m_n_iqs[antenna_path][channel_index]++; if (m_n_iqs[antenna_path][channel_index] == 1) { p_report->iq_tones[antenna_path].i_local[channel_index] = local_iq.i;
p_report->iq_tones[antenna_path].q_local[channel_index] = local_iq.q;
p_report->iq_tones[antenna_path].i_remote[channel_index] = remote_iq.i;
p_report->iq_tones[antenna_path].q_remote[channel_index] = remote_iq.q;
} else {
cumulate_mean(&p_report->iq_tones[antenna_path].i_local[channel_index],
local_iq.i, &m_n_iqs[antenna_path][channel_index]);
cumulate_mean(&p_report->iq_tones[antenna_path].q_local[channel_index],
local_iq.q, &m_n_iqs[antenna_path][channel_index]);
cumulate_mean(&p_report->iq_tones[antenna_path].i_remote[channel_index],
remote_iq.i, &m_n_iqs[antenna_path][channel_index]);
cumulate_mean(&p_report->iq_tones[antenna_path].q_remote[channel_index],
remote_iq.q, &m_n_iqs[antenna_path][channel_index]);
}
}
}
static void extract_rtt_timings(cs_de_report_t *p_report,
struct bt_hci_le_cs_step_data_mode_1 *local_rtt_data,
struct bt_hci_le_cs_step_data_mode_1 *peer_rtt_data)
{
if (local_rtt_data->packet_quality_aa_check !=
BT_HCI_LE_CS_PACKET_QUALITY_AA_CHECK_SUCCESSFUL ||
local_rtt_data->packet_rssi == BT_HCI_LE_CS_PACKET_RSSI_NOT_AVAILABLE ||
local_rtt_data->tod_toa_reflector == BT_HCI_LE_CS_TIME_DIFFERENCE_NOT_AVAILABLE ||
peer_rtt_data->packet_quality_aa_check !=
BT_HCI_LE_CS_PACKET_QUALITY_AA_CHECK_SUCCESSFUL ||
peer_rtt_data->packet_rssi == BT_HCI_LE_CS_PACKET_RSSI_NOT_AVAILABLE ||
peer_rtt_data->tod_toa_reflector == BT_HCI_LE_CS_TIME_DIFFERENCE_NOT_AVAILABLE) {
return;
}
// RSSI del paquete RTT, por paso, en dBm.
/*
LOG_INF("RTT packet RSSI: local=%d dBm, peer=%d dBm",
(int8_t)local_rtt_data->packet_rssi, (int8_t)peer_rtt_data->packet_rssi);
*/
if (p_report->role == BT_CONN_LE_CS_ROLE_INITIATOR) {
p_report->rtt_accumulated_half_ns +=
local_rtt_data->toa_tod_initiator - peer_rtt_data->tod_toa_reflector;
} else {
p_report->rtt_accumulated_half_ns +=
peer_rtt_data->toa_tod_initiator - local_rtt_data->tod_toa_reflector;
}
p_report->rtt_count++;
}
static bool process_ranging_header(struct ras_ranging_header *ranging_header, void *user_data)
{
cs_de_report_t *p_report = (cs_de_report_t *)user_data;
p_report->n_ap = MAX(1, ((ranging_header->antenna_paths_mask & BIT(0)) +
((ranging_header->antenna_paths_mask & BIT(1)) >> 1) +
((ranging_header->antenna_paths_mask & BIT(2)) >> 2) +
((ranging_header->antenna_paths_mask & BIT(3)) >> 3)));
return true;
}
static bool process_step_data(struct bt_le_cs_subevent_step *local_step,
struct bt_le_cs_subevent_step *peer_step, void *user_data)
{
cs_de_report_t *p_report = (cs_de_report_t *)user_data;
if (local_step->mode == BT_HCI_OP_LE_CS_MAIN_MODE_2) {
struct bt_hci_le_cs_step_data_mode_2 *local_step_data =
(struct bt_hci_le_cs_step_data_mode_2 *)local_step->data;
struct bt_hci_le_cs_step_data_mode_2 *peer_step_data =
(struct bt_hci_le_cs_step_data_mode_2 *)peer_step->data;
/*LOG_INF("PCT CHANNEL: physical=%u index=%u",
local_step->channel,
local_step->channel - CHANNEL_INDEX_OFFSET);*/
extract_pcts(p_report, local_step->channel - CHANNEL_INDEX_OFFSET,
local_step_data->antenna_permutation_index, local_step_data->tone_info,
peer_step_data->tone_info);
} else if (local_step->mode == BT_HCI_OP_LE_CS_MAIN_MODE_1) {
struct bt_hci_le_cs_step_data_mode_1 *local_step_data =
(struct bt_hci_le_cs_step_data_mode_1 *)local_step->data;
struct bt_hci_le_cs_step_data_mode_1 *peer_step_data =
(struct bt_hci_le_cs_step_data_mode_1 *)peer_step->data;
extract_rtt_timings(p_report, local_step_data, peer_step_data);
} else if (local_step->mode == BT_HCI_OP_LE_CS_MAIN_MODE_3) {
struct bt_hci_le_cs_step_data_mode_3 *local_step_data =
(struct bt_hci_le_cs_step_data_mode_3 *)local_step->data;
struct bt_hci_le_cs_step_data_mode_3 *peer_step_data =
(struct bt_hci_le_cs_step_data_mode_3 *)peer_step->data;
extract_pcts(p_report, local_step->channel - CHANNEL_INDEX_OFFSET,
local_step_data->antenna_permutation_index, local_step_data->tone_info,
peer_step_data->tone_info);
extract_rtt_timings(p_report,
(struct bt_hci_le_cs_step_data_mode_1 *)local_step_data,
(struct bt_hci_le_cs_step_data_mode_1 *)peer_step_data);
}
return true;
}
static void ranging_data_cb(struct bt_conn *conn, uint16_t ranging_counter, int err)
{
ARG_UNUSED(conn);
if (err) {
LOG_ERR("Error when receiving ranging data with ranging counter %d (err %d)",
ranging_counter, err);
return;
}
if (ranging_counter != most_recent_local_ranging_counter) {
LOG_INF("Ranging data dropped as peer ranging counter doesn't match local ranging "
"data counter. (peer: %u, local: %u)",
ranging_counter, most_recent_local_ranging_counter);
net_buf_simple_reset(&latest_local_steps);
k_sem_give(&sem_local_steps);
return;
}
LOG_DBG("Ranging data received for ranging counter %d", ranging_counter);
if (latest_local_steps.len == 0) {
LOG_WRN("All subevents in ranging counter %u were aborted",
most_recent_local_ranging_counter);
net_buf_simple_reset(&latest_local_steps);
k_sem_give(&sem_local_steps);
if (!(ras_feature_bits & RAS_FEAT_REALTIME_RD)) {
net_buf_simple_reset(&latest_peer_steps);
}
return;
}
memset(&m_cs_de_report, 0x0, sizeof(cs_de_report_t));
memset(m_n_iqs, 0, sizeof(m_n_iqs));
bt_ras_rreq_rd_subevent_data_parse(&latest_peer_steps, &latest_local_steps, cs_config.role,
process_ranging_header, NULL, process_step_data,
&m_cs_de_report);
for (uint8_t ap = 0; ap < m_cs_de_report.n_ap; ap++) {
m_cs_de_report.distance_estimates[ap].ifft = NAN;
m_cs_de_report.distance_estimates[ap].phase_slope = NAN;
m_cs_de_report.distance_estimates[ap].rtt = NAN;
m_cs_de_report.distance_estimates[ap].best = NAN;
if (m_is_tone_quality_ok(m_n_iqs[ap], cs_config.channel_map)) {
m_cs_de_report.tone_quality[ap] = CS_DE_TONE_QUALITY_OK;
} else {
m_cs_de_report.tone_quality[ap] = CS_DE_TONE_QUALITY_BAD;
}
}
net_buf_simple_reset(&latest_local_steps);
if (!(ras_feature_bits & RAS_FEAT_REALTIME_RD)) {
net_buf_simple_reset(&latest_peer_steps);
}
k_sem_give(&sem_local_steps);
cs_de_quality_t quality = cs_de_calc(&m_cs_de_report);
if (quality == CS_DE_QUALITY_OK) {
for (uint8_t ap = 0; ap < m_cs_de_report.n_ap; ap++) { if (m_cs_de_report.tone_quality[ap] == CS_DE_TONE_QUALITY_OK || isfinite(m_cs_de_report.distance_estimates[ap].rtt)) { store_distance_estimates_in_buffer(&m_cs_de_report.distance_estimates[ap], &distance_estimate_buffers[ap]); } } k_sem_give(&sem_distance_estimate_updated); } } static void subevent_result_cb(struct bt_conn *conn, struct bt_conn_le_cs_subevent_result *result) { if (dropped_ranging_counter == result->header.procedure_counter) {
return;
}
if (most_recent_local_ranging_counter
!= bt_ras_rreq_get_ranging_counter(result->header.procedure_counter)) {
int sem_state = k_sem_take(&sem_local_steps, K_NO_WAIT);
if (sem_state < 0) { dropped_ranging_counter = result->header.procedure_counter;
LOG_INF("Dropped subevent results. Waiting for ranging data from peer.");
return;
}
most_recent_local_ranging_counter =
bt_ras_rreq_get_ranging_counter(result->header.procedure_counter);
}
if (result->header.subevent_done_status == BT_CONN_LE_CS_SUBEVENT_ABORTED) {
/* The steps from this subevent will not be used. */
} else if (result->step_data_buf) {
if (result->step_data_buf->len <= net_buf_simple_tailroom(&latest_local_steps)) { uint16_t len = result->step_data_buf->len;
uint8_t *step_data = net_buf_simple_pull_mem(result->step_data_buf, len);
net_buf_simple_add_mem(&latest_local_steps, step_data, len);
} else {
LOG_ERR("Not enough memory to store step data. (%d > %d)",
latest_local_steps.len + result->step_data_buf->len,
latest_local_steps.size);
net_buf_simple_reset(&latest_local_steps);
dropped_ranging_counter = result->header.procedure_counter;
return;
}
}
dropped_ranging_counter = PROCEDURE_COUNTER_NONE;
if (result->header.procedure_done_status == BT_CONN_LE_CS_PROCEDURE_COMPLETE) {
most_recent_local_ranging_counter =
bt_ras_rreq_get_ranging_counter(result->header.procedure_counter);
} else if (result->header.procedure_done_status == BT_CONN_LE_CS_PROCEDURE_ABORTED) {
LOG_WRN("Procedure %u aborted", result->header.procedure_counter);
net_buf_simple_reset(&latest_local_steps);
k_sem_give(&sem_local_steps);
}
}
static void ranging_data_ready_cb(struct bt_conn *conn, uint16_t ranging_counter)
{
LOG_DBG("Ranging data ready %i", ranging_counter);
if (ranging_counter == most_recent_local_ranging_counter) {
int err = bt_ras_rreq_cp_get_ranging_data(connection, &latest_peer_steps,
ranging_counter,
ranging_data_cb);
if (err) {
LOG_ERR("Get ranging data failed (err %d)", err);
net_buf_simple_reset(&latest_local_steps);
net_buf_simple_reset(&latest_peer_steps);
k_sem_give(&sem_local_steps);
}
}
}
static void ranging_data_overwritten_cb(struct bt_conn *conn, uint16_t ranging_counter)
{
LOG_INF("Ranging data overwritten %i", ranging_counter);
}
static void mtu_exchange_cb(struct bt_conn *conn, uint8_t err,
struct bt_gatt_exchange_params *params)
{
if (err) {
LOG_ERR("MTU exchange failed (err %d)", err);
return;
}
LOG_INF("MTU exchange success (%u)", bt_gatt_get_mtu(conn));
k_sem_give(&sem_mtu_exchange_done);
}
static void discovery_completed_cb(struct bt_gatt_dm *dm, void *context)
{
int err;
LOG_INF("The discovery procedure succeeded");
struct bt_conn *conn = bt_gatt_dm_conn_get(dm);
bt_gatt_dm_data_print(dm);
err = bt_ras_rreq_alloc_and_assign_handles(dm, conn);
if (err) {
LOG_ERR("RAS RREQ alloc init failed (err %d)", err);
}
err = bt_gatt_dm_data_release(dm);
if (err) {
LOG_ERR("Could not release the discovery data (err %d)", err);
}
k_sem_give(&sem_discovery_done);
}
static void discovery_service_not_found_cb(struct bt_conn *conn, void *context)
{
LOG_INF("The service could not be found during the discovery, disconnecting");
bt_conn_disconnect(connection, BT_HCI_ERR_REMOTE_USER_TERM_CONN);
}
static void discovery_error_found_cb(struct bt_conn *conn, int err, void *context)
{
LOG_INF("The discovery procedure failed (err %d)", err);
bt_conn_disconnect(connection, BT_HCI_ERR_REMOTE_USER_TERM_CONN);
}
static struct bt_gatt_dm_cb discovery_cb = {
.completed = discovery_completed_cb,
.service_not_found = discovery_service_not_found_cb,
.error_found = discovery_error_found_cb,
};
static void security_changed(struct bt_conn *conn, bt_security_t level, enum bt_security_err err)
{
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
if (err) {
LOG_ERR("Security failed: %s level %u err %d %s", addr, level, err,
bt_security_err_to_str(err));
return;
}
LOG_INF("Security changed: %s level %u", addr, level);
k_sem_give(&sem_security);
}
static bool le_param_req(struct bt_conn *conn, struct bt_le_conn_param *param)
{
/* Ignore peer parameter preferences. */
return false;
}
static void connected_cb(struct bt_conn *conn, uint8_t err)
{
char addr[BT_ADDR_LE_STR_LEN];
(void)bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
LOG_INF("Connected to %s (err 0x%02X)", addr, err);
if (err) {
bt_conn_unref(conn);
connection = NULL;
} else {
connection = bt_conn_ref(conn);
k_sem_give(&sem_connected);
dk_set_led_on(CON_STATUS_LED);
}
}
static void disconnected_cb(struct bt_conn *conn, uint8_t reason)
{
LOG_INF("Disconnected (reason 0x%02X)", reason);
bt_conn_unref(conn);
connection = NULL;
dk_set_led_off(CON_STATUS_LED);
sys_reboot(SYS_REBOOT_COLD);
}
static void remote_capabilities_cb(struct bt_conn *conn,
uint8_t status,
struct bt_conn_le_cs_capabilities *params)
{
ARG_UNUSED(conn);
ARG_UNUSED(params);
if (status == BT_HCI_ERR_SUCCESS) {
LOG_INF("CS capability exchange completed.");
k_sem_give(&sem_remote_capabilities_obtained);
} else {
LOG_WRN("CS capability exchange failed. (HCI status 0x%02x)", status);
}
}
static void config_create_cb(struct bt_conn *conn, uint8_t status,
struct bt_conn_le_cs_config *config)
{
ARG_UNUSED(conn);
if (status == BT_HCI_ERR_SUCCESS) {
cs_config = *config;
const char *mode_str[5] = {"Unused", "1 (RTT)", "2 (PBR)", "3 (RTT + PBR)",
"Invalid"};
const char *role_str[3] = {"Initiator", "Reflector", "Invalid"};
const char *rtt_type_str[8] = {
"AA only", "32-bit sounding", "96-bit sounding", "32-bit random",
"64-bit random", "96-bit random", "128-bit random", "Invalid"};
const char *phy_str[4] = {"Invalid", "LE 1M PHY", "LE 2M PHY", "LE 2M 2BT PHY"};
const char *chsel_type_str[3] = {"Algorithm #3b", "Algorithm #3c", "Invalid"};
const char *ch3c_shape_str[3] = {"Hat shape", "X shape", "Invalid"};
uint8_t mode_idx = config->mode > 0 && config->mode < 4 ? config->mode : 4;
uint8_t role_idx = MIN(config->role, 2);
uint8_t rtt_type_idx = MIN(config->rtt_type, 7);
uint8_t phy_idx = config->cs_sync_phy > 0 && config->cs_sync_phy < 4 ? config->cs_sync_phy
: 0;
uint8_t chsel_type_idx = MIN(config->channel_selection_type, 2);
uint8_t ch3c_shape_idx = MIN(config->ch3c_shape, 2);
LOG_INF("CS config creation complete.\n"
" - id: %u\n"
" - mode: %s\n"
" - min_main_mode_steps: %u\n"
" - max_main_mode_steps: %u\n"
" - main_mode_repetition: %u\n"
" - mode_0_steps: %u\n"
" - role: %s\n"
" - rtt_type: %s\n"
" - cs_sync_phy: %s\n"
" - channel_map_repetition: %u\n"
" - channel_selection_type: %s\n"
" - ch3c_shape: %s\n"
" - ch3c_jump: %u\n"
" - t_ip1_time_us: %u\n"
" - t_ip2_time_us: %u\n"
" - t_fcs_time_us: %u\n"
" - t_pm_time_us: %u\n"
" - channel_map: 0x%08X%08X%04X\n",
config->id, mode_str[mode_idx],
config->min_main_mode_steps, config->max_main_mode_steps,
config->main_mode_repetition, config->mode_0_steps, role_str[role_idx],
rtt_type_str[rtt_type_idx], phy_str[phy_idx],
config->channel_map_repetition, chsel_type_str[chsel_type_idx],
ch3c_shape_str[ch3c_shape_idx], config->ch3c_jump, config->t_ip1_time_us,
config->t_ip2_time_us, config->t_fcs_time_us, config->t_pm_time_us,
sys_get_le32(&config->channel_map[6]),
sys_get_le32(&config->channel_map[2]),
sys_get_le16(&config->channel_map[0]));
/*LOG_INF("ACTIVE CHANNELS:");
for (uint8_t ch = 2; ch <= 76; ch++) { if (BT_LE_CS_CHANNEL_BIT_GET(config->channel_map, ch)) {
LOG_INF(" physical channel %u (%u MHz)",
ch, 2402 + ch);
}
}*/
k_sem_give(&sem_config_created);
} else {
LOG_WRN("CS config creation failed. (HCI status 0x%02x)", status);
}
}
static void security_enable_cb(struct bt_conn *conn, uint8_t status)
{
ARG_UNUSED(conn);
if (status == BT_HCI_ERR_SUCCESS) {
LOG_INF("CS security enabled.");
k_sem_give(&sem_cs_security_enabled);
} else {
LOG_WRN("CS security enable failed. (HCI status 0x%02x)", status);
}
}
static void procedure_enable_cb(struct bt_conn *conn,
uint8_t status,
struct bt_conn_le_cs_procedure_enable_complete *params)
{
ARG_UNUSED(conn);
if (status == BT_HCI_ERR_SUCCESS) {
if (params->state == 1) {
LOG_INF("CS procedures enabled:\n"
" - config ID: %u\n"
" - antenna configuration index: %u\n"
" - TX power: %d dbm\n"
" - subevent length: %u us\n"
" - subevents per event: %u\n"
" - subevent interval: %u\n"
" - event interval: %u\n"
" - procedure interval: %u\n"
" - procedure count: %u\n"
" - maximum procedure length: %u",
params->config_id, params->tone_antenna_config_selection,
params->selected_tx_power, params->subevent_len,
params->subevents_per_event, params->subevent_interval,
params->event_interval, params->procedure_interval,
params->procedure_count, params->max_procedure_len);
} else {
LOG_INF("CS procedures disabled.");
}
} else {
LOG_WRN("CS procedures enable failed. (HCI status 0x%02x)", status);
}
}
void ras_features_read_cb(struct bt_conn *conn, uint32_t feature_bits, int err)
{
if (err) {
LOG_WRN("Error while reading RAS feature bits (err %d)", err);
} else {
LOG_INF("Read RAS feature bits: 0x%x", feature_bits);
ras_feature_bits = feature_bits;
}
k_sem_give(&sem_ras_features);
}
static void scan_filter_match(struct bt_scan_device_info *device_info,
struct bt_scan_filter_match *filter_match, bool connectable)
{
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(device_info->recv_info->addr, addr, sizeof(addr));
LOG_INF("Filters matched. Address: %s connectable: %d", addr, connectable);
}
static void scan_connecting_error(struct bt_scan_device_info *device_info)
{
int err;
LOG_INF("Connecting failed, restarting scanning");
err = bt_scan_start(BT_SCAN_TYPE_SCAN_PASSIVE);
if (err) {
LOG_ERR("Failed to restart scanning (err %i)", err);
return;
}
}
static void scan_connecting(struct bt_scan_device_info *device_info, struct bt_conn *conn)
{
LOG_INF("Connecting");
}
BT_SCAN_CB_INIT(scan_cb, scan_filter_match, NULL, scan_connecting_error, scan_connecting);
static int scan_init(struct bt_scan_init_param *p_param)
{
int err;
bt_scan_init(p_param);
bt_scan_cb_register(&scan_cb);
err = bt_scan_filter_add(BT_SCAN_FILTER_TYPE_UUID, BT_UUID_RANGING_SERVICE);
if (err) {
LOG_ERR("Scanning filters cannot be set (err %d)", err);
return err;
}
err = bt_scan_filter_enable(BT_SCAN_UUID_FILTER, false);
if (err) {
LOG_ERR("Filters cannot be turned on (err %d)", err);
return err;
}
return 0;
}
BT_CONN_CB_DEFINE(conn_cb) = {
.connected = connected_cb,
.disconnected = disconnected_cb,
.le_param_req = le_param_req,
.security_changed = security_changed,
.le_cs_read_remote_capabilities_complete = remote_capabilities_cb,
.le_cs_config_complete = config_create_cb,
.le_cs_security_enable_complete = security_enable_cb,
.le_cs_procedure_enable_complete = procedure_enable_cb,
.le_cs_subevent_data_available = subevent_result_cb,
};
static void cs_config_get(struct bt_le_cs_create_config_params *config_params)
{
config_params->id = CS_CONFIG_ID;
config_params->mode = CS_CONFIG_MODE;
config_params->min_main_mode_steps = 2;
config_params->max_main_mode_steps = 10;
config_params->main_mode_repetition = 0;
config_params->mode_0_steps = NUM_MODE_0_STEPS;
config_params->role = BT_CONN_LE_CS_ROLE_INITIATOR;
config_params->rtt_type = BT_CONN_LE_CS_RTT_TYPE_96_BIT_RANDOM;
config_params->cs_sync_phy = BT_CONN_LE_CS_SYNC_1M_PHY;
config_params->channel_map_repetition = 1;
config_params->channel_selection_type = BT_CONN_LE_CS_CHSEL_TYPE_3B;
config_params->ch3c_shape = BT_CONN_LE_CS_CH3C_SHAPE_HAT;
config_params->ch3c_jump = 2;
}
static cs_de_dist_estimates_t distance_estimates_print(uint8_t ap)
{
cs_de_dist_estimates_t distance_on_ap = get_distance(ap);
#if defined(CONFIG_SAMPLE_RAS_INITIATOR_STEP_MODE_1)
LOG_INF("Latest distance estimate rtt: %.2f meters", (double)distance_on_ap.rtt);
#elif defined(CONFIG_SAMPLE_RAS_INITIATOR_STEP_MODE_2)
LOG_INF("Latest distance estimates on antenna path %u: ifft: %.2f, phase_slope: %.2f "
"meters",
ap, (double)distance_on_ap.ifft, (double)distance_on_ap.phase_slope);
#elif defined(CONFIG_SAMPLE_RAS_INITIATOR_STEP_MODE_2_SUB_MODE_1) || \
defined(CONFIG_SAMPLE_RAS_INITIATOR_STEP_MODE_3)
LOG_INF("Distancia: ifft: %.2f, phase_slope: %.2f, rtt: %.2f meters",
(double)distance_on_ap.ifft, (double)distance_on_ap.phase_slope,
(double)distance_on_ap.rtt);
#else
BUILD_ASSERT(false, "Invalid ranging mode");
#endif
return distance_on_ap;
}
int main(void)
{
int err;
LOG_INF("Starting Channel Sounding Initiator Sample");
dk_leds_init();
err = oled_init();
if (err) {
LOG_WRN("OLED init failed (err %d), continuing without display", err);
}
err = bt_enable(NULL);
if (err) {
LOG_ERR("Bluetooth init failed (err %d)", err);
return 0;
}
struct bt_scan_init_param scan_params = {
.scan_param = NULL,
.conn_param = BT_LE_CONN_PARAM(0x10, 0x10, 0, BT_GAP_MS_TO_CONN_TIMEOUT(4000)),
.connect_if_match = 1
};
err = scan_init(&scan_params);
if (err) {
LOG_ERR("Scan init failed (err %d)", err);
return 0;
}
err = bt_scan_start(BT_SCAN_TYPE_SCAN_PASSIVE);
if (err) {
LOG_ERR("Scanning failed to start (err %i)", err);
return 0;
}
k_sem_take(&sem_connected, K_FOREVER);
err = bt_conn_set_security(connection, BT_SECURITY_L2);
if (err) {
LOG_ERR("Failed to encrypt connection (err %d)", err);
return 0;
}
k_sem_take(&sem_security, K_FOREVER);
static struct bt_gatt_exchange_params mtu_exchange_params = {.func = mtu_exchange_cb};
bt_gatt_exchange_mtu(connection, &mtu_exchange_params);
k_sem_take(&sem_mtu_exchange_done, K_FOREVER);
err = bt_gatt_dm_start(connection, BT_UUID_RANGING_SERVICE, &discovery_cb, NULL);
if (err) {
LOG_ERR("Discovery failed (err %d)", err);
return 0;
}
k_sem_take(&sem_discovery_done, K_FOREVER);
const struct bt_le_cs_set_default_settings_param default_settings = {
.enable_initiator_role = true,
.enable_reflector_role = false,
.cs_sync_antenna_selection = BT_LE_CS_ANTENNA_SELECTION_OPT_REPETITIVE,
.max_tx_power = BT_HCI_OP_LE_CS_MAX_MAX_TX_POWER,
};
err = bt_le_cs_set_default_settings(connection, &default_settings);
if (err) {
LOG_ERR("Failed to configure default CS settings (err %d)", err);
return 0;
}
err = bt_ras_rreq_read_features(connection, ras_features_read_cb);
if (err) {
LOG_ERR("Could not get RAS features from peer (err %d)", err);
return 0;
}
k_sem_take(&sem_ras_features, K_FOREVER);
const bool realtime_rd = ras_feature_bits & RAS_FEAT_REALTIME_RD;
if (realtime_rd) {
err = bt_ras_rreq_realtime_rd_subscribe(connection,
&latest_peer_steps,
ranging_data_cb);
if (err) {
LOG_ERR("RAS RREQ Real-time ranging data subscribe failed (err %d)", err);
return 0;
}
} else {
err = bt_ras_rreq_rd_overwritten_subscribe(connection, ranging_data_overwritten_cb);
if (err) {
LOG_ERR("RAS RREQ ranging data overwritten subscribe failed (err %d)", err);
return 0;
}
err = bt_ras_rreq_rd_ready_subscribe(connection, ranging_data_ready_cb);
if (err) {
LOG_ERR("RAS RREQ ranging data ready subscribe failed (err %d)", err);
return 0;
}
err = bt_ras_rreq_on_demand_rd_subscribe(connection);
if (err) {
LOG_ERR("RAS RREQ On-demand ranging data subscribe failed (err %d)", err);
return 0;
}
err = bt_ras_rreq_cp_subscribe(connection);
if (err) {
LOG_ERR("RAS RREQ CP subscribe failed (err %d)", err);
return 0;
}
}
err = bt_le_cs_read_remote_supported_capabilities(connection);
if (err) {
LOG_ERR("Failed to exchange CS capabilities (err %d)", err);
return 0;
}
k_sem_take(&sem_remote_capabilities_obtained, K_FOREVER);
struct bt_le_cs_create_config_params config_params;
cs_config_get(&config_params);
bt_le_cs_set_valid_chmap_bits(config_params.channel_map);
err = bt_le_cs_create_config(connection, &config_params,
BT_LE_CS_CREATE_CONFIG_CONTEXT_LOCAL_AND_REMOTE);
if (err) {
LOG_ERR("Failed to create CS config (err %d)", err);
return 0;
}
k_sem_take(&sem_config_created, K_FOREVER);
err = bt_le_cs_security_enable(connection);
if (err) {
LOG_ERR("Failed to start CS Security (err %d)", err);
return 0;
}
k_sem_take(&sem_cs_security_enabled, K_FOREVER);
/* scale factor of conn_interval units to proc_interval units is 1.25/0.625 = 2 */
const uint16_t acl_interval_in_proc_interval_units =
scan_params.conn_param->interval_max * 2;
uint16_t desired_procedure_interval = realtime_rd ? 10 : 10;
uint16_t desired_max_procedure_length =
acl_interval_in_proc_interval_units * (desired_procedure_interval - 1);
const struct bt_le_cs_set_procedure_parameters_param procedure_params = {
.config_id = CS_CONFIG_ID,
.max_procedure_len = desired_max_procedure_length,
.min_procedure_interval = desired_procedure_interval,
.max_procedure_interval = desired_procedure_interval,
.max_procedure_count = 0,
.min_subevent_len = 16000,
.max_subevent_len = 16000,
.tone_antenna_config_selection = BT_LE_CS_TONE_ANTENNA_CONFIGURATION_A1_B1,
.phy = BT_LE_CS_PROCEDURE_PHY_2M,
.tx_power_delta = 0x80,
.preferred_peer_antenna = BT_LE_CS_PROCEDURE_PREFERRED_PEER_ANTENNA_1,
.snr_control_initiator = BT_LE_CS_SNR_CONTROL_NOT_USED,
.snr_control_reflector = BT_LE_CS_SNR_CONTROL_NOT_USED,
};
err = bt_le_cs_set_procedure_parameters(connection, &procedure_params);
if (err) {
LOG_ERR("Failed to set procedure parameters (err %d)", err);
return 0;
}
struct bt_le_cs_procedure_enable_param params = {
.config_id = CS_CONFIG_ID,
.enable = 1,
};
err = bt_le_cs_procedure_enable(connection, ¶ms);
if (err) {
LOG_ERR("Failed to enable CS procedures (err %d)", err);
return 0;
}
while (true) {
k_sem_take(&sem_distance_estimate_updated, K_FOREVER);
for (uint8_t ap = 0; ap < MAX_AP; ap++) {
if (distance_estimate_buffers[ap].num_valid != 0) {
cs_de_dist_estimates_t distance = distance_estimates_print(ap);
if (device_is_ready(oled)) {
oled_show_distance(&distance);
}
}
}
}
return 0;
}
Iniciator – prj.conf
- Modifica la configuración del archivo: prj.conf
#
# Copyright (c) 2024 Nordic Semiconductor ASA
#
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
#
CONFIG_NCS_SAMPLES_DEFAULTS=y
CONFIG_DK_LIBRARY=y
CONFIG_BT=y
CONFIG_BT_SMP=y
CONFIG_BT_CENTRAL=y
CONFIG_BT_MAX_CONN=1
CONFIG_BT_BONDABLE=n
CONFIG_BT_GATT_CLIENT=y
CONFIG_BT_GATT_DYNAMIC_DB=y
CONFIG_BT_CHANNEL_SOUNDING=y
CONFIG_BT_RAS=y
CONFIG_BT_RAS_RREQ=y
CONFIG_BT_SCAN=y
CONFIG_BT_SCAN_FILTER_ENABLE=y
CONFIG_BT_SCAN_UUID_CNT=1
# The Ranging Profile recommends a MTU of at least 247 octets.
CONFIG_BT_L2CAP_TX_MTU=498
CONFIG_BT_BUF_ACL_TX_SIZE=502
CONFIG_BT_BUF_ACL_RX_SIZE=502
CONFIG_BT_ATT_PREPARE_COUNT=3
CONFIG_BT_CTLR_DATA_LENGTH_MAX=251
# This reduces RAM usage. Additional RAM is needed to support optional
# features such as mode 3 or multiantenna. Change or remove these if
# using those features
CONFIG_BT_RAS_MODE_3_SUPPORTED=n
CONFIG_BT_RAS_MAX_ANTENNA_PATHS=1
CONFIG_BT_CS_DE_MAX_NUM_ANTENNA_PATHS=1
CONFIG_BT_CTLR_SDC_CS_MAX_ANTENNA_PATHS=1
CONFIG_BT_CTLR_SDC_CS_NUM_ANTENNAS=1
CONFIG_BT_CTLR_SDC_CS_STEP_MODE3=n
CONFIG_BT_CTLR_SDC_CS_ROLE_INITIATOR_ONLY=y
# Disabling the CS Test command reduces flash usage
CONFIG_BT_CTLR_CHANNEL_SOUNDING_TEST=n
# This allows CS and ACL to use different PHYs
CONFIG_BT_TRANSMIT_POWER_CONTROL=y
# This improves the performance of floating-point operations
CONFIG_FPU=y
CONFIG_FPU_SHARING=y
CONFIG_CBPRINTF_FP_SUPPORT=y
CONFIG_BT_CS_DE=y
CONFIG_BT_CS_DE_512_NFFT=y
# SSD1306 I2C OLED
CONFIG_I2C=y
CONFIG_DISPLAY=y
CONFIG_SSD1306=y
CONFIG_CHARACTER_FRAMEBUFFER=y
CONFIG_HEAP_MEM_POOL_SIZE=16384
Iniciator – xiao_nrf54lm20a_nrf54lm20a_cpuapp.conf
- Agrega un nuevo archivo en \boards
- Nombra el archivo como: xiao_nrf54lm20a_nrf54lm20a_cpuapp.conf
- Agrega la configuración:
#
# Copyright (c) 2025 Nordic Semiconductor ASA
#
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
#
CONFIG_HW_STACK_PROTECTION=n
CONFIG_BOOT_WATCHDOG_FEED=n
# XIAO nRF54LM20A uses SPI NOR external flash (PY25Q64)
CONFIG_GPIO=y
CONFIG_SPI=y
CONFIG_SPI_NOR=y
CONFIG_SPI_NOR_SFDP_DEVICETREE=y
CONFIG_SPI_NOR_FLASH_LAYOUT_PAGE_SIZE=4096
# Increase the maximum number of sectors to 512 to fit the big image size (> 1024 kB).
CONFIG_BOOT_MAX_IMG_SECTORS=512
# Currently, without tickless kernel, the SYSCOUNTER value after the software
# reset is not set properly and due to that the first system interrupt is not called
# in the proper time - the SYSCOUNTER value is set to the value from before
# reset + 1. Hence, the reboot time increases more and more.
# To avoid it enable tickless kernel for mcuboot.
CONFIG_TICKLESS_KERNEL=y
Iniciator – xiao_nrf54lm20a_nrf54lm20a_cpuapp.overlay
- Agrega un nuevo archivo en \boards
- Nombra el archivo como: xiao_nrf54lm20a_nrf54lm20a_cpuapp.overlay
- Agrega la configuración:
/*
* Copyright (c) 2025 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
*/
/ {
chosen {
nordic,pm-ext-flash = &py25q64;
zephyr,display = &ssd1306_128x64;
};
aliases {
/* Use watchdog wdt31 as the application watchdog */
watchdog0 = &wdt31;
};
};
&py25q64 {
status = "okay";
};
&wdt31 {
status = "okay";
};
&i2c22 {
status = "okay";
zephyr,concat-buf-size = <2048>;
ssd1306_128x64: ssd1306@3c {
compatible = "solomon,ssd1306fb";
reg = <0x3c>;
width = <128>;
height = <64>;
segment-offset = <0>;
page-offset = <0>;
display-offset = <0>;
multiplex-ratio = <63>;
segment-remap;
com-invdir;
prechargep = <0x22>;
};
};
Añade la configuración de la placa.
- Selecciona «Add build Configuration«
- Selecciona «nRF Connect SDK v3.3.0«
- Selecciona «nRF Connect SDK Toolchain v3.3.0«
- Selecciona la placa «xiao_nrf54lm20a/nrf54lm20a/cpuapp«. La encontraras seleccionando la opción «Custom«.
- Busca y selecciona el archivo «prj.conf«. Lo encontraras en la carpeta del proyecto.
- Genera el archivo «Build«.
Flashea la placa Xiao nRF54LM20A Sense
- Abre una nueva terminal seleccionando «Start New terminal» en el archivo «build» generado.
- Conecta la placa Xiao nRF54LM20A a la computadora.
- Escribe el comando «west flash –build-dir build» y ejecútalo en la terminal.
Circuito
La Pantalla OLED SSD1306 se conecta a los pines SDA => D4, SCL => D5, VCC => 3.3 V y GND => GND.
La batería LiPo de 3.3V se soldán a los pines BAT+ y BAT -.
También puedes utilizar un cable desnudo USB C macho para conectar la batería a la entrada USB C de la placa.
Código Reflector
Carga el proyecto de ejemplo desde el SDK de Nordic.
- Selecciona el SDK de Nordic.
- Crea una nueva aplicación.
- Selecciona «Copy a sample«
- Escribe «Sounding» en la barra del buscador.
- Selecciona el ejemplo «Bluetooth LE Channel Sounding Reflector with RRSP«.
- Selecciona la carpeta donde desees guardar el proyecto.
Agrega la configuración para la placa Xiao nRF54LM20A Sense
1 .- Reflector – main.c
- Modifica el código del archivo: main.c
2.- Reflector – xiao_nrf54lm20a_nrf54lm20a_cpuapp.overlay
- Agrega un nuevo archivo en \boards
- Nombra el archivo como: xiao_nrf54lm20a_nrf54lm20a_cpuapp.overlay
- Agrega el código de la configuración.
3.1.- Reflector – xiao_nrf54lm20a_nrf54lm20a_cpuapp.overlay
- Agrega un nuevo archivo en \boards
- Nombra el archivo como: xiao_nrf54lm20a_nrf54lm20a_cpuapp.overlay
- Agrega el código de la configuración.
Reflector – main.c
- Modifica el código del archivo: main.c
/*
* Copyright (c) 2024 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
*/
/** @file
* @brief Channel Sounding Reflector with Ranging Responder sample
*/
#include "zephyr/types.h"
#include "zephyr/kernel.h"
#include "zephyr/bluetooth/bluetooth.h"
#include "zephyr/sys/byteorder.h"
#include "zephyr/sys/reboot.h"
#include "zephyr/bluetooth/conn.h"
#include "zephyr/bluetooth/uuid.h"
#include "zephyr/bluetooth/cs.h"
#include "bluetooth/services/ras.h"
#include "zephyr/settings/settings.h"
#include "dk_buttons_and_leds.h"
#include "zephyr/logging/log.h"
LOG_MODULE_REGISTER(app_main, LOG_LEVEL_INF);
#define CON_STATUS_LED DK_LED1
static K_SEM_DEFINE(sem_connected, 0, 1);
static K_SEM_DEFINE(sem_config, 0, 1);
static struct bt_conn *connection;
static const struct bt_data ad[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
BT_DATA_BYTES(BT_DATA_UUID16_ALL, BT_UUID_16_ENCODE(BT_UUID_RANGING_SERVICE_VAL)),
BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME, sizeof(CONFIG_BT_DEVICE_NAME) - 1),
};
static void connected_cb(struct bt_conn *conn, uint8_t err)
{
char addr[BT_ADDR_LE_STR_LEN];
(void)bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
LOG_INF("Connected to %s (err 0x%02X)", addr, err);
if (err) {
bt_conn_unref(conn);
connection = NULL;
} else {
connection = bt_conn_ref(conn);
k_sem_give(&sem_connected);
dk_set_led_on(CON_STATUS_LED);
}
}
static void disconnected_cb(struct bt_conn *conn, uint8_t reason)
{
LOG_INF("Disconnected (reason 0x%02X)", reason);
bt_conn_unref(conn);
connection = NULL;
dk_set_led_off(CON_STATUS_LED);
sys_reboot(SYS_REBOOT_COLD);
}
static void remote_capabilities_cb(struct bt_conn *conn,
uint8_t status,
struct bt_conn_le_cs_capabilities *params)
{
ARG_UNUSED(conn);
ARG_UNUSED(params);
if (status == BT_HCI_ERR_SUCCESS) {
LOG_INF("CS capability exchange completed.");
} else {
LOG_WRN("CS capability exchange failed. (HCI status 0x%02x)", status);
}
}
static void config_create_cb(struct bt_conn *conn, uint8_t status,
struct bt_conn_le_cs_config *config)
{
ARG_UNUSED(conn);
if (status == BT_HCI_ERR_SUCCESS) {
const char *mode_str[5] = {"Unused", "1 (RTT)", "2 (PBR)", "3 (RTT + PBR)",
"Invalid"};
const char *role_str[3] = {"Initiator", "Reflector", "Invalid"};
const char *rtt_type_str[8] = {
"AA only", "32-bit sounding", "96-bit sounding", "32-bit random",
"64-bit random", "96-bit random", "128-bit random", "Invalid"};
const char *phy_str[4] = {"Invalid", "LE 1M PHY", "LE 2M PHY", "LE 2M 2BT PHY"};
const char *chsel_type_str[3] = {"Algorithm #3b", "Algorithm #3c", "Invalid"};
const char *ch3c_shape_str[3] = {"Hat shape", "X shape", "Invalid"};
uint8_t mode_idx = config->mode > 0 && config->mode < 4 ? config->mode : 4;
uint8_t role_idx = MIN(config->role, 2);
uint8_t rtt_type_idx = MIN(config->rtt_type, 7);
uint8_t phy_idx = config->cs_sync_phy > 0 && config->cs_sync_phy < 4
? config->cs_sync_phy
: 0;
uint8_t chsel_type_idx = MIN(config->channel_selection_type, 2);
uint8_t ch3c_shape_idx = MIN(config->ch3c_shape, 2);
LOG_INF("CS config creation complete.\n"
" - id: %u\n"
" - mode: %s\n"
" - min_main_mode_steps: %u\n"
" - max_main_mode_steps: %u\n"
" - main_mode_repetition: %u\n"
" - mode_0_steps: %u\n"
" - role: %s\n"
" - rtt_type: %s\n"
" - cs_sync_phy: %s\n"
" - channel_map_repetition: %u\n"
" - channel_selection_type: %s\n"
" - ch3c_shape: %s\n"
" - ch3c_jump: %u\n"
" - t_ip1_time_us: %u\n"
" - t_ip2_time_us: %u\n"
" - t_fcs_time_us: %u\n"
" - t_pm_time_us: %u\n"
" - channel_map: 0x%08X%08X%04X\n",
config->id, mode_str[mode_idx],
config->min_main_mode_steps, config->max_main_mode_steps,
config->main_mode_repetition, config->mode_0_steps, role_str[role_idx],
rtt_type_str[rtt_type_idx], phy_str[phy_idx],
config->channel_map_repetition, chsel_type_str[chsel_type_idx],
ch3c_shape_str[ch3c_shape_idx], config->ch3c_jump, config->t_ip1_time_us,
config->t_ip2_time_us, config->t_fcs_time_us, config->t_pm_time_us,
sys_get_le32(&config->channel_map[6]),
sys_get_le32(&config->channel_map[2]),
sys_get_le16(&config->channel_map[0]));
k_sem_give(&sem_config);
} else {
LOG_WRN("CS config creation failed. (HCI status 0x%02x)", status);
}
}
static void security_enable_cb(struct bt_conn *conn, uint8_t status)
{
ARG_UNUSED(conn);
if (status == BT_HCI_ERR_SUCCESS) {
LOG_INF("CS security enabled.");
} else {
LOG_WRN("CS security enable failed. (HCI status 0x%02x)", status);
}
}
static void procedure_enable_cb(struct bt_conn *conn,
uint8_t status,
struct bt_conn_le_cs_procedure_enable_complete *params)
{
ARG_UNUSED(conn);
if (status == BT_HCI_ERR_SUCCESS) {
if (params->state == 1) {
LOG_INF("CS procedures enabled:\n"
" - config ID: %u\n"
" - antenna configuration index: %u\n"
" - TX power: %d dbm\n"
" - subevent length: %u us\n"
" - subevents per event: %u\n"
" - subevent interval: %u\n"
" - event interval: %u\n"
" - procedure interval: %u\n"
" - procedure count: %u\n"
" - maximum procedure length: %u",
params->config_id, params->tone_antenna_config_selection,
params->selected_tx_power, params->subevent_len,
params->subevents_per_event, params->subevent_interval,
params->event_interval, params->procedure_interval,
params->procedure_count, params->max_procedure_len);
} else {
LOG_INF("CS procedures disabled.");
}
} else {
LOG_WRN("CS procedures enable failed. (HCI status 0x%02x)", status);
}
}
BT_CONN_CB_DEFINE(conn_cb) = {
.connected = connected_cb,
.disconnected = disconnected_cb,
.le_cs_read_remote_capabilities_complete = remote_capabilities_cb,
.le_cs_config_complete = config_create_cb,
.le_cs_security_enable_complete = security_enable_cb,
.le_cs_procedure_enable_complete = procedure_enable_cb,
};
int main(void)
{
int err;
LOG_INF("Starting Channel Sounding Reflector Sample");
dk_leds_init();
err = bt_enable(NULL);
if (err) {
LOG_ERR("Bluetooth init failed (err %d)", err);
return 0;
}
if (IS_ENABLED(CONFIG_BT_SETTINGS)) {
settings_load();
}
err = bt_le_adv_start(BT_LE_ADV_CONN_FAST_2, ad, ARRAY_SIZE(ad), NULL, 0);
if (err) {
LOG_ERR("Advertising failed to start (err %d)", err);
return 0;
}
while (true) {
k_sem_take(&sem_connected, K_FOREVER);
const struct bt_le_cs_set_default_settings_param default_settings = {
.enable_initiator_role = false,
.enable_reflector_role = true,
.cs_sync_antenna_selection = BT_LE_CS_ANTENNA_SELECTION_OPT_REPETITIVE,
.max_tx_power = BT_HCI_OP_LE_CS_MAX_MAX_TX_POWER,
};
err = bt_le_cs_set_default_settings(connection, &default_settings);
if (err) {
LOG_ERR("Failed to configure default CS settings (err %d)", err);
}
k_sem_take(&sem_config, K_FOREVER);
const struct bt_le_cs_set_procedure_parameters_param procedure_params = {
.config_id = 0,
.max_procedure_len = 1000,
.min_procedure_interval = 1,
.max_procedure_interval = 100,
.max_procedure_count = 0,
.min_subevent_len = 10000,
.max_subevent_len = 75000,
.tone_antenna_config_selection = BT_LE_CS_TONE_ANTENNA_CONFIGURATION_A1_B1,
.phy = BT_LE_CS_PROCEDURE_PHY_2M,
.tx_power_delta = 0x80,
.preferred_peer_antenna = BT_LE_CS_PROCEDURE_PREFERRED_PEER_ANTENNA_1,
.snr_control_initiator = BT_LE_CS_SNR_CONTROL_NOT_USED,
.snr_control_reflector = BT_LE_CS_SNR_CONTROL_NOT_USED,
};
err = bt_le_cs_set_procedure_parameters(connection, &procedure_params);
if (err) {
LOG_ERR("Failed to set procedure parameters (err %d)", err);
return 0;
}
}
return 0;
}
Reflector – xiao_nrf54lm20a_nrf54lm20a_cpuapp.conf
- Agrega un nuevo archivo en \boards
- Nombra el archivo como: xiao_nrf54lm20a_nrf54lm20a_cpuapp.conf
- Agrega la configuración:
/*
* Copyright (c) 2025 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
*/
/ {
chosen {
nordic,pm-ext-flash = &py25q64;
zephyr,display = &ssd1306_128x64;
};
aliases {
/* Use watchdog wdt31 as the application watchdog */
watchdog0 = &wdt31;
};
};
&py25q64 {
status = "okay";
};
&wdt31 {
status = "okay";
};
&i2c22 {
status = "okay";
zephyr,concat-buf-size = <2048>;
ssd1306_128x64: ssd1306@3c {
compatible = "solomon,ssd1306fb";
reg = <0x3c>;
width = <128>;
height = <64>;
segment-offset = <0>;
page-offset = <0>;
display-offset = <0>;
multiplex-ratio = <63>;
segment-remap;
com-invdir;
prechargep = <0x22>;
};
};
Reflector – xiao_nrf54lm20a_nrf54lm20a_cpuapp.overlay
- Agrega un nuevo archivo en \boards
- Nombra el archivo como: xiao_nrf54lm20a_nrf54lm20a_cpuapp.overlay
- Agrega la configuración:
/*
* Copyright (c) 2025 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
*/
/ {
chosen {
nordic,pm-ext-flash = &py25q64;
};
aliases {
/* Use watchdog wdt31 as the application watchdog */
watchdog0 = &wdt31;
};
};
&py25q64 {
status = "okay";
};
&wdt31 {
status = "okay";
};
Añade la configuración de la placa.
- Selecciona «Add build Configuration«
- Selecciona «nRF Connect SDK v3.3.0«
- Selecciona «nRF Connect SDK Toolchain v3.3.0«
- Selecciona la placa «xiao_nrf54lm20a/nrf54lm20a/cpuapp«. La encontraras seleccionando la opción «Custom«.
- Busca y selecciona el archivo «prj.conf«. Lo encontraras en la carpeta del proyecto.
- Genera el archivo «Build«.
Flashea la placa Xiao nRF54LM20A Sense
- Abre una nueva terminal seleccionando «Start New terminal» en el archivo «build» generado.
- Conecta la placa Xiao nRF54LM20A a la computadora.
- Escribe el comando «west flash –build-dir build» y ejecútalo en la terminal.
Resultados
Conclusiones
Durante las pruebas, se alcanzó una distancia máxima de aproximadamente 135 metros sin perder la conexión, con un error de medición cercano a 1 metro.
Aunque es posible mejorar la precisión del sistema calculando los factores de escala y los offsets mediante una regresión lineal con un mayor número de mediciones, también es recomendable realizar pruebas con diferentes antenas. Esto permitiría evaluar cómo influyen sus características y su orientación en el alcance y la precisión de las mediciones.
Documentación y recursos
- https://microchipotle.com/primeros-pasos-para-programar-la-xiao-nrf54lm20a-sense-con-nrf-connect-sdk/
- https://www.bluetooth.com/learn-about-bluetooth/feature-enhancements/channel-sounding/
- https://nrfconnectdocs.nordicsemi.com/ncs/latest/nrfxlib/softdevice_controller/doc/channel_sounding.html
- https://www-bluetooth-com.translate.goog/core-specification-6-feature-overview/?_x_tr_sl=en&_x_tr_tl=es&_x_tr_hl=es&_x_tr_pto=tc&_x_tr_hist=true
- https://github.com/nrfconnect/sdk-nrf/tree/v3.3.0/samples/bluetooth/channel_sounding
- https://wiki.seeedstudio.com/es/xiao_nrf54lm20a_getting_started/

