这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » 【NRF54L15|信道探测】2、用nRF54L15做一个"距离指示灯"

共1条 1/1 1 跳转至

【NRF54L15|信道探测】2、用nRF54L15做一个"距离指示灯"

高工
2026-09-12 15:02:21     打赏
用 nRF54L15 做一个"距离指示灯"

本文记录基于 Nordic nRF54L15 的 BLE 信道探测(Channel Sounding, CS)测距实验: 自制 Tag 靠近开发板时,开发板上的 4 颗 LED 像信号强度条一样逐颗点亮, 走远时逐颗熄灭。包含完整的环境搭建、编译、烧录、核心代码解析与实测数据, 全部步骤在 Windows 11 上验证通过。

0. 先看成果

实测曲线:Tag 匀速走远 4~5 米后走回cs_walk_demo.png

上图中红色曲线是相位斜率(phase_slope)距离估计值:拿着 Tag 从开发板旁边匀速 走远到 4~5 米再走回,读数画出一个平滑对称的"山峰"。绿色虚线是 4 档 LED 的切换 阈值——红线每跨过一条绿线,开发板上就多亮/灭一颗 LED。

演示效果

距离(实际)LED 显示
< 0.4 mLED1~4 全亮
~0.9 mLED1~3 亮
~1.4 mLED1~2 亮
~2.8 mLED1 亮
更远 / 找不到 Tag全灭(搜索时 LED1 慢闪心跳)

1. 什么是 Channel Sounding

Channel Sounding(信道探测)是蓝牙 6.0 引入的测距特性,通过 PBR(基于相位的 测距,Phase-Based Ranging)RTT(往返时间) 两种机制,在 2.4GHz 频段 的多个信道上交换带有精确时间戳/相位信息的报文,从而估算两个设备之间的距离。

相比传统的 RSSI 测距(误差动辄 3~5 米),CS 的典型精度可达亚米级,且具备 中继攻击防护能力,是数字钥匙、防丢器、"Find My" 类应用的理想技术。

CS 测距涉及两个角色:

  • Initiator(发起方):主动发起测距流程,距离数据在 Initiator 手里

  • Reflector(反射方):应答测距报文,本身不算距离

2. 硬件清单与角色分配

硬件角色说明
nRF54L15-DK (PCA10156)Initiator + LED 显示测距并驱动 4 颗板载 LED
自研 nRF54L15 TAG 板ReflectorCR2032 纽扣电池供电,越小越像"防丢标签"
nRF54LM20-DK (PCA10184)备用锚点本实验不用(注意:见踩坑记录第 1 条)

为什么用 DK 做 Initiator 而不是 Tag?因为 CS 的距离估计结果天然在 Initiator 一侧产生。要在哪块板上"显示距离",那块板就必须当 Initiator, 这样 LED 显示完全本地完成,不需要任何板间通信

3. 环境搭建(NCS v3.3.0)

关键结论:原生 Zephyr 做不了 nRF 的 Channel Sounding(开源链路层没有 CS 实现),必须使用 Nordic 官方的 NCS(nRF Connect SDK),CS 由 SoftDevice Controller(闭源协议栈)提供。

本文使用:

  • NCS v3.3.0,安装在 C:\ncs\v3.3.0(完整 west workspace)

  • NCS 配套工具链 C:\ncs\toolchains\936afb6332(内含 west / cmake / ninja / zephyr-sdk / nrfutil,官方 Toolchain Manager 安装)

安装完成后,写一个环境脚本 C:\ncs\ncs-env.sh(Git Bash 下使用),把工具链 加入 PATH:

#!/bin/bash
# NCS v3.3.0 工具链环境(Git Bash 下 source 使用)
export TC=/c/ncs/toolchains/936afb6332
export PATH="$TC:$TC/mingw64/bin:$TC/bin:$TC/opt/bin:$TC/opt/bin/Scripts:$TC/opt/nanopb/generator-bin:$TC/nrfutil/bin:$TC/opt/zephyr-sdk/arm-zephyr-eabi/bin:$TC/opt/zephyr-sdk/riscv64-zephyr-elf/bin:$PATH"
export PYTHONPATH="$TC/opt/bin;$TC/opt/bin/Lib;$TC/opt/bin/Lib/site-packages"
export NRFUTIL_HOME="$TC/nrfutil/home"
export ZEPHYR_TOOLCHAIN_VARIANT=zephyr
export ZEPHYR_SDK_INSTALL_DIR="$TC/opt/zephyr-sdk"

之后每次编译/烧录前,先:

source /c/ncs/ncs-env.sh

4. 固件组成

本实验需要两个固件,都基于 NCS 自带的 Channel Sounding 样例 C:\ncs\v3.3.0\nrf\samples\bluetooth\channel_sounding\):

固件来源烧到
ras_reflectorNCS 样例原样使用Tag(板型 nrf54l15tag/nrf54l15/cpuapp
cs_anchor复制 ras_initiator 样例后二次开发(见第 7 节)L15-DK(板型 nrf54l15dk/nrf54l15/cpuapp

cs_anchor 的工程放在 app/nrf54l15_tag/cs_anchor/(D 盘),结构就是标准 Zephyr 应用:CMakeLists.txt + prj.conf + src/main.c

5. west 编译

5.1 一个跨盘符的坑(Windows 特有)

工程源码在 D: 盘,NCS workspace 在 C: 盘。直接在 D 盘执行 west build 会踩两个坑:

  1. west 向上找到 D 盘的另一个 Zephyr workspace(原生 Zephyr 4.4), CMake 通过包注册表把 ZEPHYR_BASE 解析到错误的树,Kconfig 直接报错;

  2. 就算指定 NCS 侧的构建目录,west 内部对源码路径求相对路径时会报 ValueError: path is on mount 'D:', start on mount 'C:'

解法:建一个目录联接(junction),把 D 盘的工程"挂"进 NCS workspace

:: cmd 下执行(无需管理员)
mklink /J "C:\ncs\v3.3.0\cs_anchor" "D:\luglZephyrproject\app\nrf54l15_tag\cs_anchor"

文件实体仍在 D 盘,但 west 看到的是 C 盘路径,两个坑同时消失。

5.2 编译命令

source /c/ncs/ncs-env.sh
cd /c/ncs/v3.3.0

# Initiator + LED 显示固件 → nRF54L15-DK
west build -b nrf54l15dk/nrf54l15/cpuapp cs_anchor -d build/cs_anchor_l15dk -p always

# Reflector 固件 → 自研 Tag
west build -b nrf54l15tag/nrf54l15/cpuapp \
 nrf/samples/bluetooth/channel_sounding/ras_reflector \
 -d build/cs_reflector_tag -p always

说明:

  • -b 指定板型,nrf54l15tag 是自研板的板子定义(NCS 树里已加入)

  • -d 指定构建目录,两个固件各用一个

  • -p always  pristine 构建,改了 Kconfig/devicetree 后建议加上

编译产物(sysbuild 结构):

build/cs_anchor_l15dk/cs_anchor/zephyr/zephyr.hex      ← 约 302 KB
build/cs_reflector_tag/ras_reflector/zephyr/zephyr.hex ← 约 251 KB

6. 烧录

两块板都通过板载 J-Link 调试器烧录。电脑同时插多块板时,必须用序列号 区分目标。先用 nrfutil 查看:

source /c/ncs/ncs-env.sh
nrfutil device list

输出示例(Board version 可以区分板型):

1057728457  →  PCA10156 (nRF54L15-DK),  串口 COM34/COM35
1051829895  →  PCA10184 (nRF54LM20-DK), 串口 COM32/COM33

6.1 烧录 L15-DK(cs_anchor)

west flash -d build/cs_anchor_l15dk --dev-id 1057728457

注意:nrfutil runner 的序列号参数是 --dev-id,不是 --serial-number

6.2 烧录 Tag(cs_reflector_tag)——利用 DK 的 DEBUG OUT

自研 Tag 没有板载调试器,借助 L15-DK 的 DEBUG OUT 排针烧录:把 Tag 插到 DK 的 DEBUG OUT 上后,DK 的板载调试器会把 SWD 路由到 Tag 而不是 DK 自己 (所以烧 DK 之前务必先把 Tag 拔下来,反之亦然)。

Tag 插上 DEBUG OUT 后(注意方向),用 jlink runner 烧录,同样要显式指定序列号 (否则两块 J-Link 在场时会选错):

west flash -d build/cs_reflector_tag -r jlink --dev-id 1057728457

烧完把 Tag 拔下来,装 CR2032 电池独立供电即可。

6.3 验证

DK 的日志串口在 COM34(vcom0,115200)。连接成功后会周期性打印:

image.png


7. 核心代码解析(cs_anchor/src/main.c)

cs_anchorras_initiator 样例基础上只做了约 80 行改动。样例本身已完成 扫描 → 连接 → CS 安全配置 → 周期测距的全部流程,并在每次子事件后得到三种 距离估计(ifft / phase_slope / rtt 的中位值)。我们要做的只有三件事: 选哪个估计值、怎么映射成 LED、没连上时显示什么

7.1 估计值选择:只用 phase_slope

三种估计器的实测表现(详见第 8 节校准数据):phase_slope 全程单调、平滑; ifft 近距离会跳变;rtt 噪声最大。所以融合策略非常简单——优先 phase_slope,失效才退到 rtt:

static float fuse_distance(const cs_de_dist_estimates_t *est)
{
/*
 * Measured on this Tag/DK pair (2026-09-12): phase_slope is monotonic
 * over 0.2~4 m while ifft is not at close range, so use phase_slope
 * only; rtt is the last resort.
 */
if (isfinite(est->phase_slope)) {
 return est->phase_slope;
}
if (isfinite(est->rtt)) {
 return est->rtt;
}
return NAN;
}

7.2 距离 → LED 档位映射(带滞回)

阈值表 led_bar_bounds[] 是卷尺实测校准出来的(第 8 节),单位为 phase_slope 原始读数。档位逻辑分两步:先算目标档位,再做滞回—— 靠近时立即点亮(响应快),远离时要超过阈值 10% 才熄灭(防抖动)

/*
* Thresholds in *phase_slope units*, calibrated against a tape measure
* (midpoints between measured medians):
*   actual 0.2/0.5/1/2/4 m -> phase_slope 1.52/1.86/2.18/4.65/8.54
*/
static const float led_bar_bounds[LED_BAR_NUM_LEVELS] = { 1.7f, 2.0f, 3.4f, 6.5f };
static int led_bar_level;

static int dist_to_level(float dist)
{
for (int i = 0; i < LED_BAR_NUM_LEVELS; i++) {
 if (dist < led_bar_bounds[i]) {
  return LED_BAR_NUM_LEVELS - i;
 }
}
return 0;
}

static void led_bar_update(float dist)
{
int new_level;

if (!isfinite(dist)) {
 return;
}

new_level = dist_to_level(dist);
if (new_level > led_bar_level) {
 /* Getting closer: light up immediately. */
 led_bar_level = new_level;
} else if (new_level < led_bar_level) {
 /* Getting farther: require crossing the bound plus hysteresis. */
 float bound = led_bar_bounds[LED_BAR_NUM_LEVELS - led_bar_level];

 if (dist > bound * (100 + LED_BAR_HYSTERESIS_PCT) / 100.0f) {
  led_bar_level = new_level;
 }
}

dk_set_leds((uint32_t)((1 << led_bar_level) - 1));
}

dk_set_leds() 来自 NCS 的 DK 库(dk_buttons_and_leds.h),样例原本就调用 dk_leds_init(),4 颗 LED 对应位掩码 bit0~bit3,level 颗灯就是 (1 << level) - 1

7.3 挂载点:在打印日志处顺手更新 LED

样例里每次测距结果都会进 distance_estimates_print(),在函数末尾加一行即可:

static void distance_estimates_print(uint8_t ap)
{
cs_de_dist_estimates_t distance_on_ap = get_distance(ap);
/* …原有 LOG_INF 打印… */

led_bar_update(fuse_distance(&distance_on_ap));
}

7.4 心跳:未连接时 LED1 慢闪

样例原本用 LED1 做"已连接"常亮指示,我们把它改成搜索中心跳 一个 500ms 的 Zephyr 定时器,只要还没连上 Tag 就翻转 LED1; 连上之后 LED 全部交给距离条管理:

static void heartbeat_handler(struct k_timer *timer)
{
static bool led_on;

ARG_UNUSED(timer);

if (connection == NULL) {
 led_on = !led_on;
 dk_set_leds(led_on ? DK_LED1_MSK : 0);
}
}

static K_TIMER_DEFINE(heartbeat_timer, heartbeat_handler, NULL);

main()dk_leds_init() 之后启动:

 dk_leds_init();
k_timer_start(&heartbeat_timer, K_MSEC(HEARTBEAT_PERIOD_MS),
       K_MSEC(HEARTBEAT_PERIOD_MS));

8. 阈值校准方法

CS 样例不做任何距离校准(查了 NCS 的 cs_de 距离估计库和样例源码确认), 输出是原始估计值,存在固定偏移;且贴脸距离(< 10cm)处于天线近场区, 相位法会失真。所以阈值不能照抄"理想米数",必须实测。

方法:Tag 装电池、保持固定天线朝向,用卷尺放在 5 个定点,每个点采约 150 个 样本取中位数:

实际距离ifftphase_slopertt
0.2 m1.231.521.97
0.5 m1.801.862.28
1.0 m1.462.182.07
2.0 m1.824.656.58
4.0 m3.928.548.79

观察:

  • phase_slope 全程单调递增——这是"距离条"功能唯一需要的性质

  • ifft 在 0.5m → 1m 时读数反而下降(1.80 → 1.46),近距离不可靠

  • RTT 整体漂移大,只配做兜底

阈值取相邻测量点的中点,得到 {1.7, 2.0, 3.4, 6.5},对应实际距离大约 0.4 / 0.9 / 1.4 / 2.8 米。

对演示类应用,"单调 + 校准阈值"比"绝对精度"更重要。如果要输出真实米数, 还需要做多点拟合标定 + 补偿天线群延迟,那是另一个课题了。

9. 实测效果

拿着 Tag 从 DK 旁边匀速走远到 4~5 米、停 2 秒、再走回,50 秒 499 个样本 (原始数据 tools/cs_walk.csv):

走远-走回实测曲线

  • 红色 phase_slope 曲线平滑地走出一个"山峰",往返基本对称

  • LED 表现:走远时 4 → 3 → 2 → 1 → 0 逐颗熄灭,走回时逐颗点亮, 阈值边界附近没有抖动(滞回生效)

10. 踩坑记录

  1. 两块 Initiator 会抢同一个 Tag。CS 连接是 1:1 的。调试时 LM20-DK 也跑 着 initiator 固件,结果它先扫到 Tag 并连上,L15-DK 怎么都连不上。 演示时确保范围内只有一个 initiator。

  2. Tag 插在 DEBUG OUT 上时,烧录会烧到 Tag 而不是 DK(调试器路由被 切换)。烧 DK 前务必先拔 Tag。

  3. DK 串口是 vcom0(COM34)不是 vcom1。NCS 的样例 console 默认在 uart20,对应枚举出来的第一个虚拟串口。

  4. nrfutil runner 用 --dev-id 指定序列号;J-Link runner 在多个调试器 在场时也必须显式 --dev-id,否则直接失败。

  5. Windows 跨盘符构建:源码在 D 盘、NCS 在 C 盘时,用 mklink /J 建 junction 把工程挂进 NCS workspace(见 5.1)。

  6. 样例断连即重启:initiator 和 reflector 样例的 disconnected_cb 都会 sys_reboot(SYS_REBOOT_COLD),掉线后自动重新扫描/广播,演示时很省心, 但看日志时要意识到这一点。






关键词: NRF54L15     信道     探测     距离     Zephyr    

共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]