ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MATLAB实现QD室内毫米波信道射线追踪与CIR合成

MATLAB实现QD室内毫米波信道射线追踪与CIR合成

简介:面向毫米波射频应用的准确定性(QD)信道建模开源代码包,基于MATLAB实现,适合通信工程、无线传播与雷达方向的研究生及工程师用于信道仿真、射线追踪实验和算法复现。代码采用图像法射线追踪,聚焦镜面反射并忽略衍射,内置基于实际测量的准确定性射线扩散模型与材料库,支持基于跟踪的移动性,可读取XML、AMF格式的CAD场景,且无需额外工具箱,兼容R2016b及以上版本。压缩包共328个文件、约33.82MB,以92个m脚本、97个dat数据、68个json配置、28个csv及15个xml场景文件为主,另含amf模型与说明文档,目录按场景和示例组织。默认提供BoxLectureRoom示例,修改scenarioNameStr即可切换场景,便于快速搭建仿真与验证。已有1036人学习,可作为mmWave信道建模与射线追踪的实践底稿。

1. 从一条室内走廊的多径说起:MATLAB 里的 QD 信道到底在算什么

一条二十米长的走廊,两侧是石膏板墙,尽头一扇金属防火门,收发两端都架在 28 GHz 上。接收端沿走廊每走半个波长采一个点,接收功率会抖十几 dB——这种抖动不是噪声,是墙面和门上的镜面反射在特定位置上同相叠加或反相抵消造成的。纯统计信道模型生成不出这种抖动,因为它根本不知道墙在哪;全确定性射线追踪算得出来,却要求几何模型精确到毫米、材质参数标定到小数点后两位。

准确定性(Quasi-Deterministic,QD)通道走的是中间路线:能从几何里严格算出来的镜面反射路径作为确定性分量,粗糙表面上的漫散射用统计撒点补足,两者在时延-角度域里叠成一条信道冲激响应。这份 MATLAB 射线追踪工程做的就是这件事——输入墙面线段、材质参数和收发位置,输出每条路径的复振幅、时延、离开角与到达角,最后拼成 CIR。整份工程是系统开源的,适合做室内毫米波链路仿真、想把统计信道换成带地理信息的模型、又不想从零写一套几何引擎的人。

2. 工程骨架:从 scene 描述到信道矩阵的数据流

拿到一份 MATLAB 工程,我习惯先不跑,先把函数调用链捋一遍。这个仓库的结构非常清楚,从上到下就三层:几何层负责“墙在哪、光走哪条路”,物理层负责“这条路衰减多少、相位转了多少”,信道层负责“把这些路径按采样率摊到时延轴上,再乘以阵列导向矢量”。三层之间只靠结构体传参,没有全局变量,这一点在后期改场景的时候省了大量麻烦。

2.1 目录结构与每个函数的职责

文件职责输入 → 输出
demo_indoor.m演示入口,搭一个 20×12 m 的室内场景并画图无 → 图 +paths
cfg/default_cfg.m默认参数集中管理无 →cfg结构体
scene/build_scene.m把墙面线段和材质拼成scenecfg→scene
core/trace_specular.m镜像法递归,枚举所有镜面路径scene, tx, rx→paths
geom/reflect_point.m单次反射的镜像点求解源点、接收点、墙段 → 反射点
geom/is_blocked.m视线遮挡检测两个点、全部墙段 → 布尔值
phys/fresnel.mTE/TM 菲涅尔反射系数材质、频率、入射角 → 两个复数
core/gen_diffuse.m漫散射撒点,生成统计路径场景、散射系数、点数 →scat
core/build_cir.m路径清单 → 离散时延抽头paths, cfg→h
post/pdp.m功率时延谱与均方根时延扩展paths→P, tau, tau_rms

重点看trace_specular.m和build_cir.m这两个文件,其余都是被它们调用的工具。几何层和物理层是无状态的纯函数,你给它什么参数它返回什么结果,这种写法在做参数扫描的时候特别舒服——外层套一个parfor就能跑,不用担心中间状态被污染。

2.2 场景与材质怎么描述:线段墙 + 四个材质参数

场景没有用 CAD 模型,而是最朴素的二维线段加上材质编号。这么做的好处是几何求交可以写成闭式解,不用做网格加速,几百条路径在一台普通笔记本上几十毫秒就算完了。

% scene/build_scene.m function scene = build_scene(cfg) % 每行一条墙:[x1 y1 x2 y2 mat_id],单位统一为米 scene.walls = [ ... 0 0 20 0 1; % 下侧长墙,石膏板 20 0 20 12 1; % 右端墙 20 12 0 12 1; % 上侧长墙 0 12 0 0 1; % 左端墙 6 0 6 4 3; % 走廊中段的玻璃隔断 20 6 20 6 4]; % 尽头的金属防火门(退化成一个点,仅作示意) % 材质库:eps_r 相对介电常数,sigma 电导率(S/m),rough 表面粗糙度(m),S 散射系数 scene.materials = struct( ... 'eps_r', {4.5, 6.5, 6.0, 1.0}, ... 'sigma', {0.02, 0.15, 0.01, 1e7}, ... 'rough', {0.10, 0.30, 0.02, 0.01}, ... 'S', {0.25, 0.40, 0.10, 0.02}); end

walls的每一行是一个线段,最后两列是材质编号,材质参数在同一行的materials结构体数组里按编号索引。eps_r和sigma决定菲涅尔系数,rough只在后续做粗糙面相位扰动时用到,S是漫散射的功率占比,取值 0 到 1,越大说明这个表面越“散”。我一般按下面的量级填:石膏板eps_r≈4.5、sigma≈0.02;混凝土eps_r≈6.5、sigma≈0.15;普通玻璃eps_r≈6.0、sigma≈0.01;金属近似成理想导体,sigma给到1e7让复介电常数虚部足够大。

提示:rough和S不是标准电磁参数,是需要用实测数据反推的拟合量。如果手头只有仿真结果没有实测,先按上表的默认值跑,结果趋势是对的,绝对电平会有几 dB 偏差。

2.3 主脚本的执行顺序与 cfg 结构

所有可调参数集中在一个结构体里,不散落在各函数内部,这是我在 MATLAB 工程里最坚持的一条。参数一多,散着写迟早会出现“改了 A 文件忘了 B 文件”的翻车现场。

% cfg/default_cfg.m function cfg = default_cfg() cfg.fc = 28e9; % 载频 cfg.BW = 1e9; % 带宽,决定时延轴分辨率 cfg.Nt = 4; % 发射阵元数 cfg.Nr = 4; % 接收阵元数 cfg.lambda = 3e8 / cfg.fc; cfg.max_order = 3; % 最大反射次数,控制路径数与计算量 cfg.Nscat = 20000; % 漫散射撒点数 cfg.seed = 20240501; % 固定随机种子,保证结果可复现 cfg.tx.pos = [1.0, 2.0]; cfg.rx.pos = [15.0, 8.5]; cfg.tx.vel = [0, 0]; % 速度矢量,用于多普勒 cfg.rx.vel = [1.2, 0]; cfg.arr_sp = 0.5; % 阵元间距,单位波长 end

执行顺序是固定的五步:build_scene拿到几何,trace_specular枚举镜面路径,gen_diffuse撒点补漫散射,build_cir把两类路径合到同一根时延轴上,pdp出图。中间变量里最关键的是paths这个结构体数组,它的每个元素对应一条路径:

paths(i).tau % 总时延 = 总路程 / c0 paths(i).amp % 复振幅(含路径损耗、反射系数、相位) paths(i).aoa % 到达角(相对接收阵列法向,弧度) paths(i).aod % 离开角 paths(i).nord % 反射次数,0 表示直射 paths(i).pts % 反射点坐标,N×2,画图时用

后面要做任何后处理,比如加天线方向图、加遮挡、按簇重排序,都在这个结构体上动手,不用回头改追踪代码。这也是我把几何和信道拆成两个函数的理由:几何算完就固化下来,信道层可以反复改采样策略而不重跑追踪。

3. 镜像法射线追踪:反射路径从几何到复振幅

射线追踪的主流做法有两类,一类是正向发射射线再判断打到哪,一类是镜像法反向求解。这个工程用的是镜像法,原因很直接:二维场景下镜像点有闭式解,不需要角度分辨率和射线管半径这类玄学参数,一条路径就是一条,算出来的时延是精确值而不是采样值。代价是路径数量随反射次数的增加呈指数上涨,所以max_order一般不超过 3 到 4。

3.1 镜像点递归与路径枚举

单次反射的几何本质很简单:把发射源关于墙面做镜像,镜像点和接收点的连线与墙面的交点就是反射点。多次反射就是对镜像点再镜像,递归下去。

% core/trace_specular.m(节选) function paths = trace_specular(scene, cfg) paths = struct('tau', {}, 'amp', {}, 'aoa', {}, 'aod', {}, 'pts', {}); src = cfg.tx.pos; rx = cfg.rx.pos; nwall = size(scene.walls, 1); % 0 阶:直射路径,先判遮挡 if ~is_blocked(src, rx, scene.walls) paths(end+1) = make_path([src; rx], scene, cfg, 0); end % 1..max_order 阶:逐次镜像 for ord = 1:cfg.max_order imgs = {src}; hist = {{}}; % imgs 存当前所有镜像点,hist 存对应的墙序号序列 for k = 1:ord new_imgs = {}; new_hist = {}; for m = 1:numel(imgs) for w = 1:nwall im = mirror(imgs{m}, scene.walls(w,:)); new_imgs{end+1} = im; %#ok<AGROW> new_hist{end+1} = [hist{m}, w]; %#ok<AGROW> end end imgs = new_imgs; hist = new_hist; end % 每个镜像点回溯出实际反射点 for m = 1:numel(imgs) [pts, ok] = backtrack(imgs{m}, hist{m}, rx, scene.walls); if ok paths(end+1) = make_path(pts, scene, cfg, ord); %#ok<AGROW> end end end end

外层循环控制反射阶数,内层对每个镜像点再遍历所有墙面做二次镜像,hist记录这一串镜像是按什么顺序对哪些墙做的,回溯时要从最后一堵墙往前推反射点。backtrack里做的事情是把接收点也镜像回去,逐段求交,任何一段交点落在线段之外或者被其他墙挡住,整条路径直接丢弃。这个“任何一段不合法就整条丢”的策略看起来很粗暴,但比逐段修补要可靠得多——半条路径留着不但没用,还会污染后面的统计。

3.2 可见性裁剪:反射点必须落在墙段内

镜像法最容易写错的地方就在这里。镜像点求出来是无限长直线上的解,不代表它真的打在墙上。

% geom/reflect_point.m function [pr, ok] = reflect_point(src, dst, wall) % 二维镜像法求反射点;wall = [x1 y1 x2 y2 ...] d = wall(3:4) - wall(1:2); n = [d(2), -d(1)] / norm(d); % 墙面单位法向 if dot(src - wall(1:2), n) < 0 n = -n; % 法向统一指向源所在的一侧 end src_m = src - 2 * dot(src - wall(1:2), n) * n; % 源关于墙面的镜像 cr = @(a, b) a(1)*b(2) - a(2)*b(1); t = cr(wall(1:2) - src_m, d) / cr(dst - src_m, d); pr = src_m + t * (dst - src_m); % 交点 s = dot(pr - wall(1:2), d) / dot(d, d); % 交点在线段上的归一化位置 ok = (s >= 0) && (s <= 1); % 只在 [0,1] 内才算打在这堵墙上 end

n的方向必须统一,不然镜像点会翻到墙的另一侧,后面所有几何判断都跟着错。s是两个端点之间的归一化投影,s<0或s>1说明交点跑到墙外去了。这个判断一定要在递归回溯的每一步都做,不能只在最后一步做——中间某一段跑墙外,整条链路的相位就全是错的。

除了边界判断还要做遮挡检测:从反射点到下一个点之间,有没有别的墙横在中间。is_blocked.m就是标准的线段求交,遍历所有墙面,任意一段与连线相交且交点不在端点附近,就判为遮挡。端点附近要做容差,否则相邻两面墙的公共端点会把所有路径都判成遮挡,这是二维场景下的常见坑。

3.3 复振幅:菲涅尔反射系数与极化分解

几何算完只得到路程,要让结果有意义还得算复振幅。路径增益由自由空间扩散和每次反射的菲涅尔系数相乘得到,相位就是2πd/λ。

% phys/fresnel.m function [R_te, R_tm] = fresnel(eps_r, sigma, f, theta) eps0 = 8.8541878128e-12; omega = 2*pi*f; eps_c = eps_r - 1j * sigma / (omega * eps0); % 复介电常数,金属时虚部很大 n1 = 1; n2 = sqrt(eps_c); % 空气 -> 墙体 sin_t = n1 * sin(theta) ./ n2; cos_t = sqrt(1 - sin_t.^2); R_te = (n1*cos(theta) - n2.*cos_t) ./ (n1*cos(theta) + n2.*cos_t); % 垂直极化 R_tm = (n2*cos(theta) - n1.*cos_t) ./ (n2*cos(theta) + n1.*cos_t); % 平行极化 end
% core/make_path.m(节选):沿路径累乘反射系数 lam = cfg.lambda; L = 0; R = 1; for k = 1:ord seg = pts(k+1,:) - pts(k,:); L = L + norm(seg); % 累计路程 wi = wall_id(k); % 第 k 次反射打在哪堵墙 mat = scene.materials(wi); th = acos(abs(dot(seg/norm(seg), wall_normal(wi)))); % 入射角 [Rte, Rtm] = fresnel(mat.eps_r, mat.sigma, cfg.fc, th); % 按天线极化选择:这里假设垂直极化,换成斜极化就投个影 R = R * Rte; end p.tau = L / 3e8; p.amp = lam / (4*pi*L) * R * exp(-1j*2*pi*L/lam);

L是这条路径的总路程,lam/(4*pi*L)是自由空间衰减项,R是沿途所有反射系数的连乘,exp(-1j*2*pi*L/lam)是传播相位。注意L用的是总路程而不是直线距离,这一点在多径仿真里必须严格——差一个反射段,时延就差几百皮秒,在高带宽下会直接落到不同的抽头上。

注意:fresnel返回的是两个极化分量,实际用哪个取决于发射和接收天线的极化方向。如果天线是斜极化的,得把入射面内的两个正交分量投影到天线极化方向上再相加,直接取R_te会在交叉极化场景下偏差很大。

4. QD 合成:把路径清单压成时延-角度-多普勒的 CIR

到这一步,手上是一堆离散的路径,每条带复振幅、时延、角度。真实的接收机看到的不是这些理想冲激,而是被带宽和阵列平滑过的连续响应。QD 里“确定性”的部分指的是前面这些从几何严格算出的镜面路径,“准”字体现在后面的漫散射:粗糙表面上的散射无法用镜像法枚举,但它的统计特性——平均功率、角度扩散、时延扩散——可以从表面粗糙度和散射系数推出来。

4.1 时延轴与抽头离散化

最直接的做法是把每条路径当成一个理想冲激,按几何时延排到不同的抽头上。这么做的前提是路径之间的时延差远大于采样间隔,在 1 GHz 带宽下对应 1 ns,而室内相邻多径的时延差经常只有几百皮秒,直接取整会让几条路径挤进同一个 bin,相位一相加就相互抵消,能量凭空消失。

% core/build_cir.m function [h, tau_axis] = build_cir(paths, scat, cfg) dt = 1 / (2 * cfg.BW); % 时延分辨率,取带宽倒数的二分之一 tau_all = [paths.tau, scat.tau]; amp_all = [paths.amp, scat.amp]; Lmax = max(tau_all); tau_axis = (0:dt:Lmax).'; % 统一时延轴 h = zeros(numel(tau_axis), 1); % 每条路径按 sinc 插值到轴上,而不是直接取整 for i = 1:numel(amp_all) h = h + amp_all(i) * sinc((tau_axis - tau_all(i)) / dt); end h = h * sqrt(dt); % 能量归一,避免带宽变化时总功率漂移 end

sinc插值相当于给每条理想冲激卷上一个带宽为BW的理想低通滤波器,物理上对应接收机的成型滤波。dt取1/(2*BW)是因为低通滤波后的信号带宽翻倍,时延域分辨率要跟着减半,不然会欠采样。末尾乘sqrt(dt)是能量归一化,这一步不做的话,换成 500 MHz 带宽重跑,接收功率会莫名其妙掉一半——不是模型错了,是离散化的尺度因子没跟上。

4.2 阵列导向矢量与离开角、到达角

光有时延还构不成 MIMO 信道,还得把角度信息挂上去。这里假设均匀线阵,阵元间距为半波长。

% 到达角 -> 接收阵列导向矢量(ULA,法向为 0 度) function a = steering(phi, N, d_lambda) idx = (0:N-1).'; a = exp(-1j * 2*pi * d_lambda * idx * cos(phi)); % 相位逐阵元线性递增 end % build_cir 里对每条路径做外积 H = zeros(cfg.Nr, cfg.Nt, numel(tau_axis)); for i = 1:numel(amp_all) ar = steering(paths(i).aoa, cfg.Nr, cfg.arr_sp); at = steering(paths(i).aod, cfg.Nt, cfg.arr_sp).'; H(:, :, :) = H + reshape(ar * at, [cfg.Nr, cfg.Nt, 1]) ... .* reshape(amp_all(i) * sinc((tau_axis - tau_all(i))/dt), [1, 1, numel(tau_axis)]); end

角度用的是相对阵列法向的余弦,cos(phi)对应阵元间的波程差。这里有个容易忽略的点:aoa必须是最后一个散射点或反射点到接收机的方向,aod必须是发射机到第一个反射点的方向,中间那几段的方向跟阵列无关,不能拿总方向去算。多普勒同理,只跟最后一个到达段(接收端运动时)或第一个离开段(发射端运动时)的速度投影有关。

4.3 漫散射的统计叠加与第一次跑通

漫散射的生成方式是在所有可见表面上按面积加权撒点,每个点当作一个二次源,功率按散射系数和 Lambertian 方向性衰减。

% core/gen_diffuse.m function scat = gen_diffuse(scene, tx, rx, N, lambda) rng(20240501); % 固定种子,保证可复现 A = wall_length(scene.walls); % 每堵墙的长度(二维下即面积权重) idx = randsample(numel(A), N, true, A/sum(A)); % 按长度加权撒点 scat.tau = zeros(N,1); scat.amp = zeros(N,1); scat.pos = zeros(N,2); for k = 1:N w = scene.walls(idx(k), :); u = rand(); % 沿墙的归一化位置 scat.pos(k,:) = w(1:2) + u * (w(3:4) - w(1:2)); d1 = norm(scat.pos(k,:) - tx.pos); d2 = norm(rx.pos - scat.pos(k,:)); if is_blocked(scat.pos(k,:), rx.pos, scene.walls), continue; end S = scene.materials(w(5)).S; scat.tau(k) = (d1 + d2) / 3e8; scat.amp(k) = sqrt(S) * lambda / (4*pi*(d1 + d2)) * exp(-1j*2*pi*(d1+d2)/lambda); end end

N一般给到两万到十万量级,撒少了 PDP 的底噪会很毛糙,撒多了内存吃得厉害,两万条在 4 GB 内存的机器上是比较稳的区间。sqrt(S)而不是S是因为S定义的是功率占比,而amp是场强幅度,功率与幅度差一个平方根,这一点搞错会让漫散射分量整体强出好几 dB。

跑通只需要一行命令,前提是 MATLAB 在环境变量里:

matlab -batch "cd('qd-realization'); demo_indoor"

第一次跑会看到两张图,一张是俯视的几何与路径分布,另一张是功率时延谱。判断结果是否靠谱,先看 PDP 的形状:直射路径应该是最强的第一根抽头,时延在 30 到 60 ns 之间;后面几根来自墙面的一次反射,功率比直射低 8 到 15 dB;再往后是二次反射和漫散射,形成一段缓慢衰减的尾巴。如果尾巴比直射还强,八成是反射系数符号搞反了或者散射系数给大了。

顺手算一下均方根时延扩展,这是最容易和其他文献对上的一个指标:

P = abs(amp_all).^2; P = P / sum(P); tau_mean = sum(P .* tau_all); tau_rms = sqrt(sum(P .* (tau_all - tau_mean).^2)); fprintf('RMS delay spread = %.2f ns\n', tau_rms * 1e9);

普通办公室场景下这个值通常在 10 到 40 ns 之间,走廊这种长条形结构会更大一些,因为墙面之间的多次反射把时延拖长了。算出来几百 ns,那基本可以确定是把墙体长度单位当成毫米了。

5. 常见问题排查:五条踩过的坑

数学和代码本身都不复杂,翻车的地方几乎全在几何细节和数值约定上。下面这几条是我在调试过程中真实撞过的,按现象、原因、解决三段写,照着查能省不少时间。

5.1 几何与遮挡类问题

现象:路径数量比预期多好几倍,PDP 上出现大量时延极短、功率却不低的抽头。

原因:reflect_point里只做了镜像点求解,没有做s ∈ [0,1]的线段范围判断。无限长直线上的交点全部被当成有效反射,路径自然虚高,而且这些“反射”几何路程短,功率也不低。

解决:把范围判断写在回溯的每一步,而不是只在最后一阶检查。同时给s留一点容差,比如s >= -1e-9 && s <= 1+1e-9,否则正好打在墙端点的路径会因为浮点误差被丢掉。

现象:走廊两端明明有开口,仿真结果里开口外面却有反射路径。

原因:is_blocked只判断了线段相交,没有处理“射线正好穿过两面墙的公共端点”这种退化情况,交点落在端点附近时被跳过,等于给墙开了个洞。

解决:求交时对端点做1e-6量级的容差合并,或者干脆把每堵墙向内收缩一毫米,让公共端点错开。我一般用后者,改起来一行代码,不用担心浮点比较的各种边界。

5.2 数值与离散化类问题

现象:换一个带宽重跑,总接收功率变化很大,但路径清单明明没变。

原因:build_cir里用sinc插值后没有做能量归一化,离散时延轴上的抽样点数随带宽变化,等效于对连续信号做了不同密度的采样,总能量自然不守恒。

解决:末尾乘sqrt(dt),或者改用“归一到单位能量”的写法,把h除以其自身范数再乘上目标功率。我更倾向后者,因为带宽、采样率、时延轴长度三者任一变化都不影响最终功率。

现象:镜像阶数调到 4 以后,内存直接爆掉,或者跑一晚上没出结果。

原因:二维场景下 V 堵墙、N 阶反射的镜像数量是V^N,4 阶、6 堵墙就是 1296 个镜像点,每个点还要回溯、求交、判遮挡,复杂度是乘出来的。

解决:两件事。一是按功率阈值剪枝,每生成一个镜像点就先估算一次反射的功率衰减,低于直射路径 60 dB 的直接丢掉,不再往下递归;二是把is_blocked里的循环向量化,一次算完所有墙的交点,MATLAB 里这一改通常有三到五倍加速。

5.3 结果解读类问题

现象:PDP 画出来很漂亮,但和实测数据比整体低了 20 dB。

原因:自由空间损耗项用的是λ/(4πL),这是场强幅度因子;有些文献里给的是功率形式(λ/(4πL))^2,两者差一个平方。看别人的曲线对不上时,先确认对方画的是场强还是功率。

解决:在make_path里统一注释清楚amp是复场强幅度,pdp里再统一取模平方。工程里所有跟“dB”有关的输出都从功率走,中间过程一律保持场强,这样不会在某个环节漏掉平方。

现象:静态场景下时延扩展算出来很小,加了接收端移动后突然变得极大。

原因:多普勒频移被错误地按总路程变化计算,实际上对移动接收端而言,只有最后一段到达路径的方向参与多普勒,前面几次反射段的方向不随接收端移动而变。

解决:多普勒只取最后一段的单位方向矢量与接收端速度的点积,除以波长。这条改完,移动场景下的时延扩展和静止时应该在同一量级,只是角度域上会有展宽。

6. 二次开发:把 QD 通道接进链路仿真

这份代码的输出是paths和H,前者是纯几何的路径清单,后者是已经离散到阵元和时延轴上的信道张量。要接进链路级仿真,我建议只依赖paths,在外部自己拼 CIR——因为链路仿真往往要在同一个几何场景上扫不同的带宽、不同的阵列配置,每次重跑追踪太浪费,而paths跟这些参数是解耦的。

具体做法是写一个薄封装,把paths转成仿真器认识的抽头格式:

function ch = qd_channel(cfg, t) % 输入:cfg 含场景与阵列配置,t 为当前时刻(秒),用于时变多普勒 paths = trace_specular(build_scene(cfg), cfg); scat = gen_diffuse(build_scene(cfg), cfg.tx, cfg.rx, cfg.Nscat, cfg.lambda); ch.tau = [paths.tau, scat.tau].'; % 抽头时延 ch.amp = [paths.amp, scat.amp].'; % 复幅度 % 移动场景:按最后一段方向补多普勒相位 v = cfg.rx.vel - cfg.tx.vel; for i = 1:numel(paths) u = (cfg.rx.pos - paths(i).pts(end,:)); u = u / norm(u); ch.amp(i) = ch.amp(i) * exp(1j * 2*pi * dot(u, v) * t / cfg.lambda); end ch.aoa = [paths.aoa, zeros(1, numel(scat.tau))].'; end

t是一个外部传入的时间戳,同一份paths可以在不同t上重复调用,只更新相位,几何不用重算。这个设计对做时变信道仿真很关键——如果每个时间步都重新追踪一遍,跑一千个采样点就是一整天的计算量。

如果想扩到三维,改动集中在两处:reflect_point里的法向计算要从二维叉积换成立体几何的平面法向,墙面数据从线段[x1 y1 x2 y2]换成一个平面加一个多边形边界。后者判断交点是否在面内要用多边形内点判定,比二维的s∈[0,1]麻烦一些,但整体结构不用动,paths的字段也不用改,上层代码零改动。

参数标定上有个我常用的笨办法:先只保留直射和一次反射,把eps_r调到让 PDP 上第一根反射抽头的相对功率和实测对上,再打开二次反射和漫散射,调S让尾巴的衰减斜率对上。两轮下来,参数基本就落到合理区间了,比一上来就全局拟合要稳得多。那次因为没做分步标定、直接把六个参数一起优化,结果拟合出来的eps_r是 1.02——等于告诉我在空气里传播,白跑了两天。

从那以后我每次改这类几何信道代码,都强制先跑一个只有直射路径的 sanity check,确认tau = 距离/c、amp的模等于λ/(4πd),再往下加东西。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进