前三篇一直在追求"把距离测准",这一篇反过来:利用测不准。
1. 思路转变
传统测距应用关心的是:距离是多少、误差多大。
但 CS 的每一次测量,本质上是在探测两个节点之间的无线信道。这个信道由直射路径、墙面反射、家具散射共同构成。只要环境里的物体动了,多径结构就会变化,测出来的距离和相位就会抖动。
所以:
距离读数的抖动,不是噪声,而是环境变化的信号。
这就是「环境心跳」:不追求绝对精度,而是持续观察链路的稳定性,把"抖动"当作传感器输出。
【图片1:环境心跳系统结构】
2. 指标设计
直接用原始距离做判断不稳,因为不同距离下噪声水平不同。我们改用滑动窗口内的统计量:
| var_phase | 相位斜率距离的窗口方差 | 主判据,对人体移动敏感 |
| var_rtt | RTT 距离的窗口方差 | 辅助判据,抗多径 |
| var_ifft | IFFT 距离的窗口方差 | 对照 |
| 运动标志 | 连续 N 个窗口超阈值 | 驱动 LED / 蜂鸣器 |
窗口长度取 32 个样本。按 CS 每个 procedure 一次输出,实测约 10 Hz,窗口覆盖约 3 秒,正好对应"人走过去"这个时间尺度。
2.1 阈值怎么定
先采一段静止基线,观察 var_phase 的分布,再取一个能分开基线和事件的值。我们的实测数据:
| 静止基线 | 中位数 0.025,最大 0.066 |
| 人靠近 / 穿行 | 0.13–0.23,最高 0.81 |
| 人体遮挡(2 m) | 标准差从 0.029 升到 0.056 |
最终固件只用相位方差做主判据:
#define MOTION_VAR_PHASE 0.10f /* 基线最大 0.066,事件从 0.13 起 */ #define MOTION_HOLD 3 /* 连续 3 个窗口超阈值才判为运动 */
RTT 的基线方差实测在 0.22–0.29 m²,远高于相位,不能作为运动判据,只用于展示。
3. 固件实现
工程在 code/article04_env_heartbeat/,基于 NCS 官方的 ras_initiator 改造。
【图片2:环境心跳固件流程】
3.1 改了什么
| main.c 头部 | 引入 env_stats.h |
| store_distance_estimates_in_buffer() | 每存一个距离样本,同时喂给统计模块 |
| main() | 初始化统计模块 |
| while (true) 主循环 | 用 CSV 输出替代原来的 LOG_INF,并驱动 LED2 |
补丁由 tools/patch_env_heartbeat.py 自动完成,锚点找不到会直接报错,避免在错误版本上"假装成功"。
3.2 统计模块
void env_stats_push(env_stats_t *s, const cs_de_dist_estimates_t *est)
{
s->ifft[s->idx] = est->ifft;
s->phase[s->idx] = est->phase_slope;
s->rtt[s->idx] = est->rtt;
...
}方差计算会跳过 NaN:CS 在信号质量差时可能给不出有效值,直接算会污染统计。
3.3 输出格式
每行一条,方便直接存 CSV:
CSV,<uptime_ms>,<ap>,<ifft>,<phase>,<rtt>,<var_ifft>,<var_phase>,<var_rtt>,<motion>
示例:
CSV,182340,0,1.214,1.187,1.402,0.0021,0.0038,0.0092,0 CSV,182377,0,1.208,1.176,1.398,0.0023,0.0142,0.0101,0
最后一位从 0 变 1,就是"检测到环境扰动"。
3.4 编译
west build -b nrf54lm20dk/nrf54lm20b/cpuapp \ code/article04_env_heartbeat -d build/env_heartbeat
编译结果(NCS v3.4.1):
FLASH: 287204 B / 2036 KB (13.78%) RAM: 67824 B / 511 KB (12.96%)
当前进度(2026-10-27):已完成端到端实测。双板在 2.3 m 间距下稳定运行,实时检出并播放语音提示,用户确认每次回到座位都能触发。XIAO 的启动崩溃已通过直启 0x0 + 收窄 SRAM 到 160 KB + RAS 缓冲增至 8 个解决。详见 4.4 节。
4. 实验设计
4.1 三个场景
| 静止基线 | 无人、门窗关闭,采集 60 秒 | var_phase 平稳 |
| 人靠近 / 离开 | 人走向或离开两节点所在的位置,重复 10 次 | var_phase 出现明显尖峰 |
| 开关门 | 在链路侧方开关门,重复 10 次 | 幅度较小但可辨识 |
4.2 记录表
| 静止 | 0.025 | 0.066 | — | 0%(无事件) |
| 人靠近 / 穿行 | 0.025 | 0.23 | 约 9× | 实时演示全部触发 |
| 开关门 | — | — | — | 未单独测量 |
4.3 IMU 交叉验证
XIAO nRF54L15 Sense 上有 6 轴 IMU(LSM6DS3TR-C)。在反射端固件里加了 20 Hz 采样线程,两路数据用主机时间戳对齐。
实测结果(32 秒):
手/人体在链路附近挥动:CS 的 var(phase) 从 0.05 升到 0.5–0.7,而 IMU 幅值保持在 9.9–10.1 m/s²,基本不动
平移 XIAO 本体:IMU 幅值升到 28.7 m/s²,CS 抖动同时升到 0.7–1.0
原始值的零延迟相关系数约 0.01;把 IMU 也做成 3.2 秒滑动方差后,相关系数约 0.18
结论:CS 抖动的主要来源是人体/手部引起的多径变化,不是板子自身的机械振动;IMU 的价值在于区分"环境动了"和"节点动了"。
【图片3:CS 抖动与 IMU 幅值的时间对齐】
4.4 实测:桌面存在感知(2026-10-27)
布置
LM20-DK 作 Initiator,XIAO nRF54L15 作 Reflector
两板间距 2.3 m,XIAO 用充电头独立供电(不接电脑)
人坐在两板之间,动作是「起身走远 → 走回坐下」
数据
| 有效样本 | 905 |
| 时长 / 速率 | 91.2 s / 9.9 Hz |
| 运动检出(0→1) | 11 次 |
| var_phase 中位数 | 0.179 |
| var_phase 最大值 | 0.809 |
| var_phase > 0.5 的样本 | 49 |
报警链路
语音由 edge-tts 预生成:edge-tts --voice zh-CN-XiaoxiaoNeural --text "检测到有人经过" --write-media assets/audio/alert.mp3
tools/cs_alert.py 监听串口 CSV,在 motion 由 0 变 1 时用 afplay 播放,带 6 秒冷却
实测中每次走回座位都能触发,用户主观确认「每次都能正常检测到」
反射端自恢复:断连后由工作队列重新广播,10 秒硬件看门狗兜底;实测拔掉 LM20 后 XIAO 数秒内恢复广播,无需人工插拔电源
【图片4:实时检测曲线】
结论
在这个布置下,人坐在两个节点之间,LOS 被身体遮挡,距离读数本身已经不可信(IFFT/Phase 都远离真值);但 var_phase 的抖动特征依然稳定地反映了人的移动。这说明「用心跳而不是用距离」的思路是成立的:测距失效的场景,恰恰是抖动感知最有价值的地方。
5. 已知局限
采样率有限。 CS 的 procedure 间隔决定了上限,做不到音频级采样,只能看 1–10 Hz 的环境变化。
绝对阈值依赖环境。 换房间要重新标定基线。
距离影响灵敏度。 近距离时直射路径占主导,遮挡带来的变化反而更明显;远距离时多径占比高,基线噪声更大。
温度漂移。 长时间测量时晶振与器件温漂会带来慢变分量,需要高通或去趋势。
反射端自恢复已解决。 早期固件在断连后需要重新上电;现已改为断连工作队列重广播 + 10 秒硬件看门狗,实测拔掉对端后数秒内自动恢复广播,正常使用不再需要人工插拔电源。
6. 小结与下一步
这一篇把 CS 从"测距工具"变成了"环境传感器":
用滑动窗口方差而不是瞬时距离做判断
用 LED 做本地反馈,用 CSV 记录原始数据
用 IMU 做交叉验证
如果继续做,可以往三个方向走:
多节点组网,做区域定位与轨迹
把抖动特征喂给轻量分类器,区分"人/门/宠物"
结合 nRF54LM20B 的 Axon NPU 做端侧推理
附:本系列回顾
| 1 | 开箱与环境 | 远程编译 + 本地烧录链路 |
| 2 | BLE 基础 | 广播、连接、NUS |
| 3 | CS 入门 | 双板测距与误差分析 |
| 4 | 环境心跳 | 用链路抖动感知环境 |
我要赚赏金
