简介:这套Matlab仿真代码针对频率选择性衰落信道下的多用户OFDM-DCSK功率分配问题,其中OFDM即正交频分复用、DCSK即差分混沌键控。代码共11个文件,包含10个.m脚本与1张PNG示意图,压缩包仅35KB,轻量易用,已有140人学习下载。资源专为通信工程、电子信息、数学等专业学生打造,可支撑课程设计、期末大作业或毕业设计。采用参数化编程,注释明细、编程思路清晰,无需额外工具箱即可直接运行,配套案例数据便于快速复现仿真;同时兼容Matlab 2014/2019a/2024a等多个版本,方便不同环境使用。脚本覆盖单用户/多用户性能对比、用户数增长、功率分配迭代及误码率曲线绘制等方向,能帮助读者深入理解频率选择性衰落对系统性能的影响,掌握多用户功率分配算法的完整实现思路,并为后续改进或扩展研究提供基础。
1. 频率选择性衰落信道中多用户 OFDM-DCSK 系统的功率分配:先别急着点开 matlab 代码包
“频率选择性衰落信道中多用户 OFDM-DCSK 系统的功率分配”这个标题扔进搜索框,最常命中的是一个 MATLAB 代码包,rar 压缩包里装着主程序、信道脚本和几份参考资料。你把代码从网盘拉下来,在 matlab 2023b 里点开才发现,大多例程只在等功率或 AWGN 下能跑;一旦换成频率选择性信道,误码率就开始“吊车尾”。这里的问题不在 OFDM,也不在混沌,而在功率分配——深衰落子载波该给多少功率,强弱用户之间如何权衡,是这套 OFDM-DCSK 能否真正落地的分水岭。
我按自己的复现顺序讲一遍模型、代码、参数和翻车记录。读者是打算做混沌调制或非相干 OFDM 仿真的研究生,以及刚拿到 rar 包但不知道从哪改起的工程师。看完你可以直接把它改成你自己的仿真框架。
2. 从 DCSK 到 OFDM-DCSK:先建对频率选择性信道模型
2.1 DCSK 在频率选择性信道里的困境
DCSK(差分混沌键控)的基本思想很朴素:把一个比特拆成两段混沌信号,前半段是参考,后半段是携带数据比特的混沌信号,数据为 1 时后半段与前半段相同,为 0 时取反。接收端不需要信道估计,不需要载波同步,只要把前后两段做相关再判符号就能恢复数据。这是它在低功耗、低成本场景里受欢迎的根本原因。
问题也出在这个相关上。频率选择性信道的多径时延如果比较长,参考信号会通过多径叠加到后面的数据段上,造成符号间干扰(ISI),同时数据段的拖尾也会污染下一帧的参考。相关不再只对应一个比特,而是把前后几个路径的能量都卷了进来。结果是误码率出现平台,加大发射功率也压不下去。
OFDM-DCSK 的常规做法,是让每个子载波独立承载一个 DCSK 符号流。发送端把混沌参考和数据样本放到同一个子载波上,用 OFDM 的循环前缀去吸收多径时延。这样在每个子载波里,原来的频率选择性衰落被打散成了复数常数增益 H_n,DCSK 的相关可以在频域做,既保留了非相干接收的优点,也不用面对多径 ISI。代价是子载波之间增益差异很大,有的子载波落进深度衰落,信噪比可能比平均水平低一个量级。
多用户接入则在这个框架上做子载波分组:每个用户分到一段连续子载波,或者按交错方式分配。用户之间由子载波的正交性天然隔离,不需要额外的多用户检测。功率分配要解决的问题变成:总功率有限,不同用户的子载波增益不同,谁该多给,谁该少给,或者干脆不给。
2.2 频率选择性信道的离散模型与子载波增益计算
仿真里最常用的是抽头延迟线模型。信道冲激响应写成离散形式:
h(n) = Σ_{l=0}^{L-1} a_l δ(n - l)
其中 L 是多径抽头数,a_l 是第 l 条路径的复增益。为了贴近真实场景,路径功率通常按指数衰减,越晚到的路径平均功率越低。OFDM 系统的子载波间距和采样率确定后,每个子载波上的等效信道就是 h(n) 的 N 点离散傅里叶变换:
H_n = Σ_{l=0}^{L-1} a_l e^{-j2π n l / N}
这条式子写出来简单,但做仿真时大部分人第一个踩的坑是信道归一化。如果 h 的总能量不为 1,H_n 的平均幅度就不是 1,后面算信噪比、算注水的 gamma 参数全会偏。我习惯先把 h 归一化到单位能量,再去做 FFT。
下面是生成子载波增益的 MATLAB 函数,可以直接放到代码包的主目录里:
function H = per_subcarrier_channel(N, L) % N: FFT 长度,也就是子载波总数 % L: 多径抽头数,每个抽头间隔一个采样周期 % 返回 H(1:N),每个子载波上的复信道增益 h = (randn(1, L) + 1j * randn(1, L)) / sqrt(2); % 复高斯抽头 pdp = exp(-(0:L-1) / L); % 指数功率延迟谱 h = h .* sqrt(pdp); % 每条路径按功率加权 h = h / norm(h); % 总能量归一化到 1 H = fft(h, N); % 补零后做 FFT end说明一下参数:N 是 OFDM 子载波数,也是 FFT 点数;L 是多径抽头数,抽头间隔取一个采样周期,避免引入小数倍延迟,让 fft 结果更干净。pdp 用 exp(-l/L) 衰减,L 越大,信道越平坦;L 越小,频率选择性越强。norm(h)是欧几里得范数,归一化后 h 的能量为 1,这样 Hn 的平均模值大约为 1,做信噪比计算时不会整体偏移。
有些代码包里会直接用H = fft(h, N),但 h 没有归一化,结果不同信道长度下误码率曲线差好几个 dB,这不是调制的问题,是能量定义不统一的问题。
2.3 子载波增益分布决定了功率分配的方向
在 OFDM-DCSK 仿真里,接收端经过 CP 删除和 FFT 后,每个子载波上的输入输出关系可以近似写成:
Y_n = H_n X_n + W_n
这里 W_n 是频域噪声。对某一条多径信道,随便画一下 abs(H).^2 随子载波索引的变化,就能看到有些子载波处于深谷,增益可能比峰值低 10 dB 以上。如果所有子载波等功率发送,处在峡谷里的子载波信噪比会非常差,对应的用户误码率被拉高。
功率分配的目的就是根据 abs(H_n).^2 重新分配各子载波功率。经典注水算法会把更多功率投到增益好的子载波上,少给甚至不给深衰落子载波。这个思路直接套在单用户系统上没问题,多用户系统里还要考虑用户之间的公平性,后面第四章单独讲。
3. 用 MATLAB 把多用户功率分配跑通:注水算法与 OFDM-DCSK 帧结构
3.1 代码文件怎么拆,主仿真入口写在哪个文件
拿到 rar 包之后先不要急着把所有代码塞进一个脚本。我一般按五个文件拆,这样改参数和换算法都不用手忙脚乱:
- params.m:集中定义 N、K、beta、cp_len、L、P_total 等参数
- channel_gen.m:生成 H 和 h
- waterfill.m:注水功率分配
- ofdm_dcsk_link.m:发送、信道、接收的核心链路
- run_sim.m:主仿真入口,循环跑 BER,画曲线
主仿真入口只需要调用两个函数,一个算功率,一个跑链路。先用一个小脚本来验证代码能跑通,再逐步加蒙特卡洛循环。
3.2 注水功率分配函数的 MATLAB 实现
注水算法的目标是在总功率约束下最大化容量,结果是把功率分配给“信道噪声比”较高的子载波。每个子载波上的等效信道噪声比定义为:
gamma_n = abs(H_n)^2 / N0
N0 是噪声功率谱密度,仿真里就是频域每个子载波上的噪声方差。功率分配的解是:
p_n = max(mu - 1/gamma_n, 0)
mu 是水位线,需要满足总功率约束 sum(p_n) = P_total。mu 没有闭式解,用二分法找。
function p = waterfill(gamma, P_total) % 标准注水功率分配 % gamma : 子载波信道噪声比向量,长度 N % P_total : 总发射功率 % 返回 p : 分配给每个子载波的功率 gamma = gamma(:); % 统一成列向量 N = length(gamma); gamma(gamma <= 0) = eps; % 避免除零,深度为零当作极小值 mu_lo = 1 / max(gamma); % 最低水位 mu_hi = P_total + 1 / min(gamma); % 一个足够高的水位上界 for it = 1:200 mu = (mu_lo + mu_hi) / 2; p = max(mu - 1 ./ gamma, 0); if abs(sum(p) - P_total) < 1e-8 break; elseif sum(p) < P_total mu_lo = mu; % 功率不够,水位抬高 else mu_hi = mu; % 功率超了,水位压低 end end p = max(mu - 1 ./ gamma, 0); % 最终功率,负数截断为 0 end这段代码的关键在于 mu_lo 和 mu_hi 的选取。mu_lo 取 1/max(gamma),保证任何子载波上 p 至少可能为 0;mu_hi 取 P_total 加 1/min(gamma),在极端深衰落时足够大。200 次迭代对二分查找完全是够用的,大部分情况几十次就收敛了。
调用时直接传 abs(H).^2 / N0。注意 gamma 里不能有负值,所以先把小于等于 0 的用 eps 替代,对结果的影响可以忽略。这样分配完的 p 之和与 P_total 的误差在 1e-8。
3.3 多用户 OFDM-DCSK 的发送和接收核心链路
下面这段代码模拟多用户 OFDM-DCSK 系统的一轮传输,包括比特生成、混沌参考生成、子载波功率映射、频域信道叠加和 DCSK 相关判决。为了聚焦功率分配逻辑,这里用频域等效信道,也就是默认 CP 足够长且同步理想。
function [ber, p, H] = sim_multiuser_ofdm_dcsk(N, K, N_sub_user, beta, L, P_total, EbN0_dB) % 参数说明: % N : OFDM 子载波总数 % K : 用户数 % N_sub_user : 每个用户占用的子载波数,总子载波数 K*N_sub_user % beta : DCSK 参考段长度,每比特共 2*beta 个 OFDM 符号 % L : 多径抽头数 % P_total : 总发射功率 % EbN0_dB : 比特信噪比,单位 dB N_sub = K * N_sub_user; % 实际使用的子载波数 sub_alloc = cell(K, 1); for k = 1:K sub_alloc{k} = (k-1)*N_sub_user + (1:N_sub_user); % 连续子载波分组 end H = per_subcarrier_channel(N, L); % 生成子载波频域增益 noise_var = 10^(-EbN0_dB / 10); % 噪声功率 gamma = abs(H(1:N_sub)).^2 / noise_var; % 只对有用户的部分做功率分配 p = waterfill(gamma, P_total); % 注水功率 % 随机产生每个用户 1 个比特 bits = randi([0 1], K, 1); bits_mod = 2 * bits - 1; % 0 映射为 -1,1 映射为 1 % 混沌参考序列,先用高斯白噪声代替混沌序列 c_ref = randn(K, beta); % 每个用户一个 beta 长的参考段 % 构造发送频域符号 X,大小 N_sub x 2*beta X = complex(zeros(N_sub, 2*beta)); for k = 1:K idx = sub_alloc{k}; for t = 1:beta X(idx, t) = sqrt(p(idx)) .* c_ref(k, t); X(idx, t+beta) = sqrt(p(idx)) .* c_ref(k, t) .* bits_mod(k); end end % 频域等效信道,加复高斯噪声 Y = H(1:N_sub).' .* X + sqrt(noise_var/2) * (randn(size(X)) + 1j * randn(size(X))); % DCSK 相关判决 bit_hat = zeros(K, 1); for k = 1:K idx = sub_alloc{k}; corr = real(sum(sum(Y(idx, 1:beta) .* conj(Y(idx, beta+1:2*beta)), 2), 1)); bit_hat(k) = double(corr > 0); end ber = mean(bit_hat ~= bits); % 这一轮仿真的误比特个数 end先看发送端。X 的行是子载波,列是时间。前 beta 列放参考段,后 beta 列放数据段。我这里用 randn 生成高斯序列代替真正的混沌序列,因为 randn 的序列均值为零、方差为 1,与混沌序列的统计特性接近,先把链路跑通再换 logistic 映射或 Chebyshev 映射都行。
功率 p 是按子载波给出的,每个用户所占用子载波的功率来自 waterfill 的结果。这实现的是子载波间功率分配,也就是频域功率控制,而不是用户级功率控制。
接收端相关判决那三行要拆开看。Y(idx, 1:beta)取该用户所有子载波上的参考段,Y(idx, beta+1:2*beta)取数据段。两者共轭相乘,先对子载波维求和,再对时间维求和,得到一个相关值 corr。大于 0 判为比特 1,否则判为比特 0。由于数据段由参考段乘以 bits_mod,正的 bits_mod 会让相关为正,负的会让相关为负,所以判决是无偏的。
这段代码里没有显式的 IFFT 和循环前缀操作,是因为频域等效模型的计算结果和完整 OFDM 链路在 CP 足够长时完全一致。真实系统验证时再补成时域模型,原理我在最后一章讲。
3.4 主仿真脚本与蒙特卡洛循环
单次调用 sim_multiuser_ofdm_dcsk 只能得到一个比特的判决结果,要画 BER 曲线必须跑蒙特卡洛。需要注意的是每轮都要重新生成 H 和混沌序列,否则只是测了某一条固定信道下的噪声平均。
N = 64; % 子载波总数 K = 4; % 用户数 N_sub_user = 16; % 每个用户 16 个子载波 beta = 16; % DCSK 参考长度 L = 6; % 多径抽头数 P_total = 1; % 总功率归一化为 1 EbN0_dB = 0:2:12; ber_user = zeros(K, length(EbN0_dB)); trials = 500; for i = 1:length(EbN0_dB) ber_tmp = zeros(K, 1); for t = 1:trials [ber_one, ~, ~] = sim_multiuser_ofdm_dcsk(N, K, N_sub_user, beta, L, P_total, EbN0_dB(i)); ber_tmp = ber_tmp + ber_one; end ber_user(:, i) = ber_tmp / trials; end semilogy(EbN0_dB, mean(ber_user, 1), 'o-');因为每轮只给每个用户发一个比特,trials 要足够大,否则曲线抖动得厉害。简单起见可以改成每轮发几十个比特,统计误码个数再累加,这比多跑几轮更快。实际 I 建议把 sim 函数改成“每个用户一次发 N_bit 个比特”,然后把相关判决改成矩阵运算,效率会高很多。
4. 调参数也是调性能:功率分配里的迭代公差、用户公平性和信道参数
4.1 影响功率分配结果的五个关键参数
| 参数 | 常见取值 | 它影响什么 |
|---|---|---|
| N(子载波数) | 64 | 频率分辨率,子载波越多,单个子载波带宽越窄,频域增益越平坦 |
| L(多径抽头数) | 6 左右 | 信道频率选择性强度,L 越大,不同子载波增益差异越大 |
| cp_len | 大于等于 L-1 | CP 不够长会直接破坏 OFDM 子载波正交性 |
| beta(DCSK 参考长度) | 8~32 | 扩频增益,beta 越大,相关越平滑,BER 越低,但有效速率越低 |
| N0(噪声功率) | 10^(-EbN0/10) | 它直接进入 gamma 计算,N0 一旦算错,功率分配结果整体偏移 |
cp_len 没有出现在 sim 函数里,但实际 OFDM 链路里它是最容易埋雷的参数。频率选择性信道最大时延超过 CP,前一个 OFDM 符号的拖尾会落到后一个符号的 FFT 窗口里,子载波正交性被破坏。DCSK 相关对象变成了混着前后符号的混合体,BER 曲线会迅速拉平。我的经验是 cp_len 最少取 L+2,留出一点裕度。
beta 对功率分配的影响比较隐蔽。beta 越大,每个比特分散到更多 OFDM 符号上,频域相关时噪声被平滑得更多。也就是说,在功率分配固定时,beta 提高会让所有用户整体 BER 下降,但它不会改变不同子载波之间的相对好坏。功率分配算法本身只依赖 gamma,gamma 与 beta 无关。
4.2 注水算法的两个必调边界:二分容差与迭代上限
第 3 章 waterfill 函数里有三个数字按下去值得你自己调一调:mu_lo、mu_hi 和 1e-8 容差。
mu_lo 取 1/max(gamma) 是理论下限;mu_hi 我给的非常宽,是 P_total 加 1/min(gamma)。如果信道里有一个子载波增益特别小,min(gamma) 会非常小,mu_hi 会非常大,二分查找前期要白白折半好多次。这不会让结果出错,但会让 200 次迭代的最后一两次精度受到影响。可以改成 mu_hi = P_total + 1 / max(gamma),在绝大多数仿真里也够用,收敛速度更快。
迭代上限 200 次不是随便写的。二分法的收敛速度按区间宽度指数下降,mu_lo 到 mu_hi 的范围即使大 100 倍,40 次迭代也足够把误差压到 1e-8 以下。200 次纯粹是防止极端情况下死循环。如果这段代码跑得很慢,问题通常不在迭代次数,而是 gamma 向量很短时 for 循环里反复创建临时变量,可以改成直接向量化。
容差 1e-8 是绝对误差,P_total 如果设成 1,这个容差是够的。如果你把 P_total 设成 1000,1e-8 会过于严格,导致迭代 200 次都不 break,最后返回的 p 总功率误差仍在可接受范围但浪费了大量算力。更稳妥的做法是用相对误差:abs(sum(p) - P_total) < 1e-8 * P_total。
4.3 用户公平性:纯注水会饿死弱用户
标准注水只看单个子载波的 gamma,不看它属于哪个用户。如果一家用户的信道整体很差,它占用子载波的 gamma 排在最末位,很可能分不到任何功率。这在仿真里表现为某个用户的 BER 一直 0.5,其他用户曲线正常。
这时需要从“最大化和速率”转向“最大化最小用户速率”或“比例公平”。一种常见做法是把注水公式改成乘以用户权重 w_k:
p_n = max(mu - 1 / (w_k * gamma_n), 0)
w_k 取值大,则该用户会被优先分配。w_k = 1 时退化为标准注水。权重更新的常见做法是迭代:每轮仿真后统计各用户速率,把速率低的用户权重调大,速率高的调小,重复几次。这个迭代在频率选择性信道里通常跑 5 到 10 轮就能稳定。
在 MATLAB 代码包里真正难的不是写这个加权注水,而是判断当前算法是不是“假公平”。我见过不少代码在等功率下所有用户曲线一致,但一开加权注水反而差别变大,原因是权重没有归一化到同一个功率尺度。权重应满足平均关系 sum(w_k) / K = 1,否则总功率约束被破坏。代码里可以在每次更新权重后做一次归一化:w = w / mean(w)。
5. 避坑手册:多用户 OFDM-DCSK 仿真里最常见的 5 个翻车点
5.1 误码率被卡在 0.5,相关判决永远判负
现象:BER 固定为 1,或者稳定在 0.5,不管信噪比怎么调都上不去。
原因:DCSK 的数据段和参考段在构造时乘反了,或者比特映射搞反。代码里 bits_mod 为 1 时相关为正,为 -1 时相关为负,如果 bits_mod 的定义是 0 对应正相关,那判决结果会和真实比特正好相反。
解决:先打印一次相关值 corr 和真实 bits,逐行核对。最简单的方法是临时去掉噪声并设置信道为 H 全 1,跑一帧看判决是否完全一致,一致再开启噪声和真实信道。
5.2 功率分配后总功率不等于设定值
现象:输出 p 求和后比 P_total 大或小百分之几。
原因:waterfill 函数里的容差为绝对 1e-8,当 P_total 很大时循环永远不满足条件,最后直接返回最后一步的 p,而最后一步的 p 可能还是在误差边界上。另一种原因是 gamma 里有负数或 NaN,导致 max 函数忽略异常值。
解决:把退出条件改成相对误差,同时检查 gamma 里所有值是否是有限正数。可以在 waterfill 函数开头加一行 assert(all(isfinite(gamma)), 'gamma 存在非有限值')。
5.3 不同用户 BER 曲线差异大得离谱
现象:注水功率分配后,三个用户 BER 性能好的发紫,第四个用户一直很差,但信道增益看起来并没有那么悬殊。
原因:子载波分配和功率分配的位置索引不一致。常见的翻车写法是功率分配时用了 H(1:N_sub),而给第四个用户分配子载波时用了 H(17:32) 之外的索引,两者错位导致第四个用户拿到的“功率”其实来自别的子载波。
解决:打印 p 和 abs(H(1:N_sub)).^2 两个向量,按用户分组求和,确认每个用户总功率与对应子载波增益确实匹配。尤其在代码包从原作者的 32 子载波改成 64 子载波时,这种索引错误最隐蔽。
5.4 打开 rar 包里的 .m 文件中文注释乱码
现象:在 matlab 2023b 中打开下载的 .m 文件,注释是一堆乱码,运行还报“输入字符串无效”。
原因:源码文件是 GBK 编码保存的,MATLAB 的编辑器和运行环境在某些系统语言下默认按 UTF-8 解析。这是中文网络流传代码包的老毛病,和代码逻辑无关。
解决:把该 .m 文件用文本编辑器打开,另存为 UTF-8 编码;或者直接删掉注释里的中文,只保留英文注释。运行一般不受影响,乱码只影响阅读。如果文件里出现字符串常量里的中文,另存为 UTF-8 后还要检查 MATLAB 能正确识别。
5.5 真机运行报找不到 msvcp140.dll 或者 mex 文件无法加载
现象:代码包里有 .mexw64 文件,运行时一调用就报“由于找不到 msvcp140.dll,无法继续执行代码”。
原因:mex 文件是别人机器上编译的,依赖的 Visual C++ 运行库和你本机不一致,或者 MATLAB 版本跨得太远。mex 文件必须与你的 MATLAB 版本严格匹配,不是所有 rar 包里的 mex 都能直接用。
解决:自己重新编译。如果找不到源码,就直接跳过 mex 调用,用纯 .m 脚本实现相同功能。对 OFDM-DCSK 的仿真来说,几乎没有必须用 mex 的环节,纯 .m 代码在 64 子载波下跑几千次也不算太慢。
6. 让结果可信:验证功率分配增益的两个对比实验和一个调试习惯
验证功率分配做得好不好,最直接的办法是拿它跟等功率分配放在同一组信道里对比。定义等功率分配为 p_equal = P_total / N_sub,保持系统参数完全相同,只改功率向量,分别统计误码率。如果注水效果好,深衰落用户和平均用户之间的 BER 差距会明显缩小,整体 BER 曲线也会比等功率低一到两格。这里要特别注意蒙特卡洛次数必须一致,否则曲线低可能是统计抖动,不是算法功劳。
第二个实验是验证“分配结果是否真的跟随信道”。把信道 H 的某个子载波人为衰减到接近零,再看对应子载波的分配功率应该是 0。用这个办法可以快速判断 waterfill 函数有没有抽象对了信道。同样,如果把某用户全组的 gamma 都乘 0.5,功率分配后这组的平均功率应当下降,但也别降成零,否则公平性需要调整。
我自己的习惯是“黄金路径”调试法。所有多用户仿真都先跑一个简化到最低配置的版本:单用户、单子载波、无噪声、无信道衰落,确认 DCSK 相关能完美恢复比特;然后打开噪声,确认 BER 在 0 dB 附近不会出现 0.5 的异常平台;再加入频率选择性信道,确认 H 的频率选择性可以通过注水被补偿;最后才上多用户分组和公平性约束。每一步只引入一个变量,出问题时能立刻锁定在哪一层。
平时改代码包时我也会做一个简单测试:固定同一组 H,把功率分配 p 打印出来存成 .mat,换一个算法(比如把注水替换成均等功率)时再导出一次,用 abs 和 diff 对比两次功率分配的差异。这样的验证比单纯看误码率曲线更直接,因为误码率是几百次随机的平均值,而功率分配差异是确定性的,能更早发现问题。
最后说一句我自己的习惯:每次修改仿真代码,我会先跑一次只有两比特的小循环,使用一个固定随机种子,打印每一步中间变量。等到中间变量全部与手算一致,才把它改成蒙特卡洛长循环跑完。这个习惯帮我省下来的调试时间,远比写代码本身多。希望帮到你。
本文还有配套的精品资源,点击获取