
简介mD_track-master项目聚焦于利用WiFi信号中的AOA到达角与TOF飞行时间技术实现室内人体定位追踪适合无线定位研究者和MATLAB开发者。AOA通过多天线估计信号到达角度TOF则依据飞行时间换算距离两者结合可提升定位精度压缩包共含8个文件以7个.m脚本和1个README.md为主涵盖信号生成、CSI仿真、到达角/飞行时间计算与定位验证等环节项目基于MATLAB实现虽然仅6KB但核心函数较为完整便于直接阅读和二次开发。目前已有345人学习对于想理解AOATOF融合定位原理、或基于现有WiFi基础设施构建低成本室内追踪方案的读者这套代码提供了可运行的实验骨架结合README可快速梳理算法流程与仿真环境搭建思路也可作为进一步扩展多天线定位算法的起点对室内定位算法研究、毕业设计或工程验证均有一定参考价值。1. 用 WiFi 信号做人体追踪mD_track 这个 MATLAB 项目在解决什么问题室内定位里有个很尴尬的现实GPS 在楼里基本是废的蓝牙信标要布一堆节点UWB 精度好但硬件成本不低。mD_track 给了一条相对省钱的路子——直接用现成 WiFi 信号的 AOA到达角和 TOF飞行时间做人体定位追踪。仓库里 8 个 m 文件把「仿真环境搭建 → 模拟 CSI 生成 → AOA 角度估计 → TOF 距离估计 → 主流程融合」整条链路都串了起来核心就是 compute_AOA.m、compute_TOF.m 这两个文件mD_track.m 负责把它们拧成一套可跑的定位流程。这个项目适合两类人一类是做 WiFi 感知、室内定位课题的研究生想快速有个能改能跑的 MATLAB 基线而不是从零啃阵列信号处理另一类是嵌入式或无线工程师想评估不换硬件、只靠 WiFi CSI 能不能做出可用的定位方案。mD 我理解是 mobile Device 的缩写也就是面向移动设备的跟踪场景。你不需要有真实网卡和天线阵列也能先跑通整套算法后面接 Intel 5300 或 AX200 的实测 CSI 也只是替换数据输入的问题。2. 先把 AOA 啃下来到达角估计的核心逻辑与 compute_AOA.m 的实现2.1 为什么选 AOA 而不只靠 RSSI早期 WiFi 定位几乎都拿 RSSI 做距离估计但室内多径环境下RSSI 的波动能到 ±10dB 以上换算成距离误差经常是三五米起步做人体追踪根本不够看。AOA 走的是另一条路它不关心信号强不强只关心信号到达不同天线时的相位差。当目标信号斜着打到天线阵列上时相邻天线之间存在波程差 d·sinθ这个波程差会反映成相位差 2π·d·sinθ/λ。只要天线数量够、相位测得准角度就可以反推出来。WiFi 网卡通过 CSI 恰好能拿到每个子载波上的复数信道响应幅度和相位都在里面。所以 AOA 方案对多径衰落不那么敏感因为相位关系比幅度关系稳定得多。这里有个前置条件一个 AP 至少要两根天线而且天线间距最好接近半波长。两根天线只能解一个含混的角度实际项目中我一般要求至少 34 根天线做阵列这样才有余量用 MUSIC 这类超分辨算法。mD_track 里的 compute_AOA.m 走的就是这个方向。2.2 compute_AOA.mMUSIC 谱估计的完整流程AOA 估计的常见做法是 MUSIC 算法核心思想很直白把接收信号的协方差矩阵做特征分解信号子空间和噪声子空间正交然后扫描角度找谱峰。我按 mD_track 里这个函数的常见实现拆一下结构大概是这样的function theta compute_AOA(csi_matrix, params) % csi_matrix: [N_rx, N_sub, N_pkt] 复数 CSI 数据 % params.N_rx : 接收天线数 % params.N_sub : 子载波数 % params.num_sig : 信源个数目标数量 % params.angle_step: 角度扫描步长单位度 N_rx params.N_rx; N_pkt size(csi_matrix, 3); % 跨子载波合并抑制频率选择性扰动 X squeeze(mean(csi_matrix, 2)); % [N_rx, N_pkt] % 构造协方差矩阵 R (X * X) / N_pkt; % [N_rx, N_rx] % 特征分解区分信号子空间与噪声子空间 [E, D] eig(R); [~, idx] sort(diag(D), descend); E E(:, idx); E_noise E(:, (params.num_sig1):end); % 噪声子空间 % 角度扫描构造阵列流型计算 MUSIC 谱 theta -90:params.angle_step:90; P zeros(size(theta)); for k 1:length(theta) a exp(1j * 2*pi * params.ant_spacing / params.lambda ... * sind(theta(k)) * (0:N_rx-1).); P(k) 1 / abs(a * E_noise * E_noise * a); end [~, peak_idx] max(P); theta theta(peak_idx); end这段代码里有几个关键点。第一先对子载波维做均值这是因为单个子载波上的 CSI 相位受频率选择性衰落影响很大跨子载波平均能把噪声压下去让相位关系更接近理想模型。第二特征分解后取小特征值对应的特征向量构成噪声子空间MUSIC 谱的峰值位置就是信号入射方向。参数上params.ant_spacing 是天线间距典型值取 0.5 倍波长params.lambda 是载波波长2.4GHz 下约 12.5cm5GHz 下约 5.2cm。angle_step 设 0.5 度比较合适太密计算慢太疏角度量化误差大。2.3 参数怎么调天线间距、子载波选取、快拍数MUSIC 看着美好实际跑起来参数不对就是另一回事。天线间距是最容易翻车的地方严格按照半波长设置时角度覆盖范围能到 -90°90°如果间距超过半波长谱峰会出现周期性栅瓣产生「鬼影角」。仿真代码里把这个值写死成 0.5 倍波长是最稳妥的。子载波选取同样有讲究。WiFi 的 OFDM 信号里靠近频带边缘的子载波信道估计质量差导频子载波和空子载波也混在里面。compute_AOA.m 里跨子载波平均之前应该先把这些坏子载波剔掉。我一般会预留一个 valid_sub 索引掩码只让中间 70% 的子载波参与计算。快拍数决定了协方差矩阵的秩是否够。MUSIC 要求快拍数大于天线数否则协方差矩阵秩亏特征分解就分不出信号子空间和噪声子空间。仿真里一个数据包算一个快拍至少取 2050 个包做平均不然谱峰会又矮又糊。] 注意这里有个常被忽略的问题实际 WiFi 网卡的 CSI 更新率有限快拍太多会引入目标移动造成的相位模糊所以快拍数和实时性之间要取平衡一般 30 个快拍就够用了。提示如果只有两根天线MUSIC 退化成求相位差公式是 θ arcsin(Δφ·λ/(2πd))别硬套多天线 MUSIC效果不会好。3. TOF 测距不是掐秒表compute_TOF.m 的两种时间估计路线3.1 TOF 与 RSSI 测距的本质区别TOF 的思路是测信号从发射端到接收端的飞行时间再乘以光速得到距离。它和 RSSI 测距最大的区别在于RSSI 依赖路径损耗模型的准确性而 TOF 只依赖时间测量精度理论上不受发射功率和环境遮挡影响。但 WiFi 场景下 TOF 有个绕不开的物理限制带宽决定时间分辨率。一个 40MHz 带宽的 WiFi 信号IFFT 变换到时间域后每个时间bin 的宽度是 1/40MHz 25ns换算成距离是 7.5 米。也就是说直接看时域峰值的原始分辨率根本做不到米级精度必须靠频域相位信息来细化。这是理解 compute_TOF.m 的关键前提——它不是在时域里掐秒表而是在频域里求相位的斜率。CSI 的相位在理想情况下满足一个线性关系φ(f) -2π·f·τ φ₀其中 τ 就是飞行时间φ₀ 是初始相位偏移。只要对子载波频率做线性拟合斜率除以 2π 就是 TOF。这个方法的精度可以做到远优于带宽限制因为相位拟合利用了所有子载波的信息。3.2 路线一CSI 相位斜率线性拟合相位斜率法是 compute_TOF.m 里最直接的一条实现路线核心就三步取相位、解缠绕、线性拟合。function dist compute_TOF(csi_vector, params) % csi_vector: [N_sub, 1] 单个天线的 CSI % params.f_sub : 各子载波频率单位 Hz % params.valid_idx: 有效的子载波索引 f params.f_sub(params.valid_idx); H csi_vector(params.valid_idx); % 提取相位并解缠绕避免 2π 跳变 phi unwrap(angle(H)); % 线性拟合相位 -2*pi*tof * f phi0 p polyfit(f(:), phi(:), 1); tof -p(1) / (2*pi); % 单位秒 dist tof * 3e8; % 距离单位米 endpolyfit 返回的 p(1) 是斜率斜率取负再除以 2π 得到飞行时间乘光速就是距离。这里有个细节unwrap 是按相邻子载波的相位差来补 2π 跳变的但如果子载波间隔太大相位差超过 πunwrap 就会失效。所以 20MHz 带宽下的 64 个子载波没问题但如果只取了 30 个子载波Intel 5300 的典型配置子载波间隔变大unwrap 出错的风险也随之上升。我习惯在拟合之前先画一眼相位曲线确认它是干净的线性而不是一堆乱跳的点。如果相位曲线明显杂乱说明 CSI 质量不行拟合出来的 TOF 没有意义不如直接丢弃这一帧。3.3 路线二IFFT 变换域峰值另一条路线是把 CSI 当频域响应反变换到时域得到信道冲激响应CIR峰值位置对应主径的 TOF。这条路线物理意义直观但正如前面说的分辨率受带宽限制。function [tof, cir] compute_TOF_ifft(csi_vector, params) % csi_vector: [N_sub, 1] % 加窗压低旁瓣再用 IFFT 转时域 win hann(length(csi_vector)); cir ifft(csi_vector .* win, params.ifft_size); % ifft_size 可取 1024 % 找峰值位置做抛物线插值细化 [~, idx] max(abs(cir)); if idx 1 idx length(cir) % 三点抛物线插值 y0 abs(cir(idx-1)); y1 abs(cir(idx)); y2 abs(cir(idx1)); delta 0.5 * (y0 - y2) / (y0 - 2*y1 y2); idx idx delta; end tof_bin idx - 1; % 从 0 开始的 bin 序号 tof tof_bin / params.BW; % 时间 bin / 带宽 endIFFT 的 bin 宽度等于 1/BW加窗能压低旁瓣但会稍微展宽主峰所以峰值位置用抛物线插值细化一下是常规操作。实际效果上IFFT 法在低信噪比下比线性拟合更稳因为它用了所有子载波的能量而不是只依赖相位关系但高信噪比下线性拟合的精度上限更高。两条路线的取舍我的经验是如果 CSI 相位质量好用线性拟合如果相位脏、有随机跳变用 IFFT 峰值更可靠。mD_track 里 compute_TOF.m 我建议两种都实现跑完对比一下再选。4. 让整套算法跑起来仿真环境、CSI 生成与 mD_track.m 主流程4.1 simulation_environment.m搭一个室内定位试验场没有真实硬件之前先把仿真环境搭起来这套逻辑在 mD_track 里由 simulation_environment.m 负责。它的职责是定义房间大小、AP 位置、天线阵列几何、目标运动轨迹以及信道相关的物理参数。function params simulation_environment() % 房间与 AP 布局 params.room_size [12, 10]; % 房间长宽单位米 params.n_ap 2; % AP 数量 params.ap_pos [2.0, 3.0; 8.0, 7.0]; % 每个 AP 的坐标 [x, y] % 天线阵列参数 params.n_rx_ant 4; % 每个 AP 的接收天线数 params.ant_spacing 0.5; % 归一化天线间距相对波长 params.fc 5.18e9; % 载波频率 5.18GHz params.lambda 3e8 / params.fc; % 波长约 5.8cm % 信号参数 params.n_sub 52; % 有效子载波数 params.bw 40e6; % 信道带宽 40MHz params.f_sub (0:params.n_sub-1) * params.bw / params.n_sub; % 子载波频率 % 目标运动轨迹匀速直线方便验证 params.traj_start [2.0, 2.0]; params.traj_end [10.0, 8.0]; params.target_v 0.8; % 移动速度单位 m/s end这套参数里最关键的是天线间距的归一化写法。params.ant_spacing 0.5 表示实际间距是 0.5 倍波长换算成米就是 0.5 × 0.058 2.9cm这个值在真实设备上基本可做。AP 数量设 2 是因为 AOA 定位只需要两个 AP每个 AP 提供一条角度射线两条射线的交点就是目标位置如果你要更高的鲁棒性可以加到 3 个 AP 做最小二乘融合。4.2 simulation_generateCSI.m test_signal.m造一批带真实几何信息的 CSI仿真环境定义完之后simulation_generateCSI.m 负责根据目标真实位置生成 CSI。生成逻辑是逆向的先根据几何关系算出真实的 AOA 和 TOF再反推 CSI 的相位和幅度最后叠加上噪声模拟真实信道。function csi simulation_generateCSI(params, target_pos) % target_pos: [x, y] 目标当前坐标 % 返回 csi: [n_ap, n_rx_ant, n_sub] csi zeros(params.n_ap, params.n_rx_ant, params.n_sub); noise_power 10^(-params.snr_db / 10); % snr_db 默认 20 for ap 1:params.n_ap % 目标到 AP 的真实角度和距离 dx target_pos(1) - params.ap_pos(ap, 1); dy target_pos(2) - params.ap_pos(ap, 2); dist sqrt(dx^2 dy^2); aoa atan2(dy, dx) * 180 / pi; tof dist / 3e8; % 每个子载波构造 CSI天线相位差 传播时延 for sub 1:params.n_sub ant_phase 2*pi * params.ant_spacing ... * (0:params.n_rx_ant-1). * sind(aoa); exp_phase exp(1j * ant_phase) ... * exp(-1j * 2*pi * params.f_sub(sub) * tof); csi(ap, :, sub) exp_phase sqrt(noise_power/2) * ... (randn(params.n_rx_ant, 1) 1j*randn(params.n_rx_ant, 1)); end end end这段代码的物理含义很清楚天线相位差项里带了 aoa时延项里带了 tof所以生成出来的 CSI 同时包含角度信息和距离信息。噪声用复高斯白噪声模拟snr_db 越高后面 AOA 和 TOF 估计的误差越小。test_signal.m 在这种场景里一般用来做单径验证——只生成一条直达径不加多径用来确认算法链路的正确性。| 参数 | 值 / 范围 | 说明 | |-----------------|--------------|------------------------------| | snr_db | 5 ~ 30 | 信噪比低于 10 时峰值变糊 | | n_sub | 52 | 有效子载波数剔除了导频 | | target_v | 0.8 m/s | 模拟行人慢走速度适配追踪 | | 多径数量 | 1直达径 | test_signal.m 用于链路验证 |4.3 mD_track.m 主流程AOA 和 TOF 是怎么串起来的主流程文件 mD_track.m 干的是调度活先生成环境参数再沿轨迹逐帧生成 CSI对每个 AP 分别算 AOA 和 TOF最后把角度和距离信息融合成坐标估计。function track_result mD_track(params) % 初始化轨迹 t_total norm(params.traj_end - params.traj_start) / params.target_v; n_frame round(t_total * params.fps); track_result zeros(n_frame, 2); for frame 1:n_frame % 当前真实位置 t (frame - 1) / params.fps; target_pos params.traj_start ... (params.traj_end - params.traj_start) * (t / t_total); % 生成 CSI csi simulation_generateCSI(params, target_pos); % 每个 AP 独立做 AOA 和 TOF aoa_est zeros(1, params.n_ap); dist_est zeros(1, params.n_ap); for ap 1:params.n_ap csi_ap squeeze(csi(ap, :, :)); % [n_rx_ant, n_sub] aoa_est(ap) compute_AOA(csi_ap, params); dist_est(ap) compute_TOF(mean(csi_ap, 1), params); % TOF 用单天线即可 end % 用两个 AP 的 AOA 射线求交点TOF 作为距离校验 tan_a1 tand(aoa_est(1)); tan_a2 tand(aoa_est(2)); x (params.ap_pos(2,2) - params.ap_pos(1,2) ... tan_a1*params.ap_pos(1,1) - tan_a2*params.ap_pos(2,1)) ... / (tan_a1 - tan_a2); y params.ap_pos(1,2) tan_a1 * (x - params.ap_pos(1,1)); track_result(frame, :) [x, y]; end end角度射线交点的几何融合是 AOA 定位的经典做法。每个 AP 测到一个角度相当于从 AP 位置射出一条方向线两条方向线的交点就是目标位置。TOF 在这个主流程里的角色更像校验——我一般会拿 TOF 反推的距离和角度射线交点算出的距离做对比如果二者误差超过 2 米这一帧大概率是某个 AP 的角度估计飞了直接丢帧比强行融合更划算。提示两个 AP 的 AOA 射线平行时会解出无穷远处实际代码里要对 |tan 差| 做下限保护小于 0.05 时这一帧标记为无效。5. 避坑手册AOA/TOF 定位项目最常见的五个翻车现场5.1 翻车一AOA 谱出现对称鬼影角现象MUSIC 谱里除了真实角度附近有个峰在 -θ 位置也冒出一个几乎等高的峰角度估计有时跳到鬼影那边轨迹出现左右镜像跳变。原因天线阵列流型本身存在前后向模糊。均匀线阵的角度响应不是唯一的θ 和 180°-θ 产生的相位差相同另外当天线间距大于半波长时还会出现周期性栅瓣。解决天线间距严格设为半波长以内用多径环境中的多个快拍做平均随机噪声的影响会被压低如果仍出现镜像对称峰用 TOF 距离信息做辅助判断——真实的 AOA 对应的射线交点应该在房间范围内鬼影角算出的交点在房间外直接把越界的判别为无效帧。5.2 翻车二TOF 始终偏大一个固定量现象无论目标站在哪里TOF 估计距离都稳定偏大 50ns 左右换算成距离约 15 米明显不合理。原因WiFi 网卡收发链路里有固定的处理时延包括基带滤波、射频前端的群时延这个时延对每个包都一样属于系统偏差。还有可能是仿真里忘了把发射端到接收端的参考时延清零。解决先做一次标定——把目标放在已知距离 d₀ 处测出 TOF 估计值得到偏置 Δτ之后每次估计都减掉这个偏置。仿真环境里直接检查生成 CSI 时有没有额外叠加固定时延。这一步不做后面定位误差再调算法都压不下去属于最典型的血泪经验。5.3 翻车三相位 unwrap 失败导致 TOF 曲线跳跃现象TOF 估计结果随时间剧烈跳动前后两帧能差出几十纳秒画出来像全是毛刺的噪声。原因unwrap 是逐点判断相位差是否超过 π如果子载波间隔大真实相位差超过 πunwrap 会把不该补的 2π 补上或者漏补拟合斜率就错了。Intel 5300 这类设备只上报 30 个子载波间隔比标准 64 点 FFT 大问题更容易暴露。解决不要直接对原始相位 unwrap。先对 CSI 做共轭相乘——用相邻子载波之间的乘法代替直接相位差再对这个差分序列拟合。差分法对常数相位偏移完全不敏感也不需要 unwrap实现还更简单。5.4 翻车四MUSIC 谱峰淹没在噪声底里现象协方差矩阵算出来后整个 MUSIC 谱高高低低真实角度附近没有明显尖峰噪声底高得离谱。原因快拍数和信噪比不匹配协方差矩阵估计不准另一个常见原因是目标信号相关比如同一个目标的多条路径相干导致协方差矩阵秩亏。解决先做空间平滑——把天线阵列分成若干重叠子阵列对每个子阵列的协方差矩阵求平均这能有效解除信号的相干性。快拍数至少加到 30信噪比低于 10dB 时不要指望 MUSIC 出好结果可以退回求简单相位差法。5.5 翻车五仿真精度很好实测完全对不上现象仿真里 AOA 误差不到 2 度TOF 误差不到 1ns定位误差 30cm换成真实采集的 WiFi CSI 之后定位误差直接飙到 3 米以上。原因仿真默认了理想条件——天线间距精确、通道相位响应平坦、时钟同步。真实 WiFi 设备有三样东西是仿真给不了的载波频率偏移CFO、采样时钟偏移SFO、天线间的相位不一致性。解决在 simulation_generateCSI.m 里主动叠加随机的 CFO 和 SFO让仿真先贴近脏数据真实数据接入时先用已知位置的静态目标做一次全链路标定校准每个天线的相位偏移。把「实测比仿真差一个量级」当作默认预期而不是意外后面调试心态会稳很多。6. 再进阶一点AOD 联合估计与定位精度的验证方法6.1 compute_AOD.m 在定位链路里的角色AOD 是信号离开发射端的角度和 AOA 是同一个发射事件的两端。在 WiFi 定位里加 AOD 估计主要解决的是单边定位的镜像模糊问题。AOA 只能告诉你信号从哪个方向来但不能告诉你在 AP 的哪一侧——如果目标在 AP 正上方AOA 的 ±90° 其实是同一个方向。联合 AOD 之后相当于多了一条射线约束交点唯一性更强也不会再出现 5.1 节那种镜像鬼影。6.2 定位精度验证清单跑通 mD_track 之后验证精度别只看平均误差。我每次都会画一张 CDF 曲线看 50% 分位和 95% 分位这比均值有意义得多——均值会被少数烂帧拉低而 CDF 能暴露「大部分时间精度不错但 5% 的时间飞了」这种问题。function plot_cdf(errors) % errors: 每帧定位误差单位米 % 按误差升序排列计算累积概率 sorted_err sort(errors); cdf (1:length(sorted_err)) / length(sorted_err); figure; plot(sorted_err, cdf, LineWidth, 1.5); grid on; xlabel(定位误差 (m)); ylabel(累积概率); title(定位误差 CDF); end配合这套验证我还会统计三个数RMSE、50% 误差、95% 误差。如果 95% 误差超过均值的三倍说明有周期性坏帧优先查 AOA 谱峰是否在个别帧失效如果 50% 和 95% 都大才是算法模型本身的问题。从那以后我每次跑 WiFi 定位算法之前都强制自己先走一遍「画相位曲线、看 MUSIC 谱、算 CDF」这三个动作再谈调参和融合。一个项目值不值得下代码能不能跑通只是一半另一半是你有没有一套自己的验证习惯希望帮到你。本文还有配套的精品资源点击获取