ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

QAM软解调MATLAB仿真:从LLR计算到误码率优化

QAM软解调MATLAB仿真:从LLR计算到误码率优化 简介这是一份面向通信工程、电子信息类专业学生及数字通信初学者的QAM调制与软解调Matlab实现包。资源围绕QAM调制原理和软解调流程通过随机二进制生成、星座映射、QAM调制、软判决检测、四符号映射及成形滤波等多个m文件完整演示了从发射端到接收端的软比特计算与LLR生成过程适合用于课程设计、毕业设计或通信系统仿真入门。压缩包共8个文件均为.m脚本整体仅4KB结构精简便于直接阅读和修改运行。目前已有515人学习下载。相比单纯理论讲解这套代码提供了可直接运行的模块化示例读者可借此理解16-QAM/64-QAM等星座映射规则掌握软解调中概率信息与对数似然比的实现方式并进一步结合Turbo码或LDPC码进行误码性能仿真实验是兼顾原理与实战的实用参考资料。1. 从硬判决到软解调这套 QAM 仿真代码能帮你把误码率降下来做通信系统仿真的人应该都有过这种经历明明把 LDPC 码接在了后面误码率曲线却怎么都压不下去查来查去发现解调器还在用最朴素的硬判决。硬判决把每个接收符号直接怼到最近的星座点上只剩一个“是或不是”的答案后面跟着的信道译码器根本拿不到可靠性信息纠错能力直接打对折。QAM 软解调解决的就是这个问题——它把每个比特的似然信息以软比特形式交出去让 Turbo 码、LDPC 码能把概率用起来。我手上这套 QAM 调制解调源码就是干这个的八个 MATLAB 文件从随机二进制序列生成、符号映射、星座图绘制到软解调输出链路完整适合正在做基带仿真或者想搞懂软比特怎么算的人直接复现。2. QAM 调制与软解调原理软比特不是玄学是对数似然比的工程化落地2.1 从星座点到软信息硬判决丢掉了什么先看最基本的 16-QAM 调制。四个比特组合成一个符号映射到星座图上某个复数点比如比特组合0000对应坐标 (-3, -3)0001对应 (-3, -1)。发射机做的事情很机械查表把一个四比特组合变成复数再乘上成形滤波系数发出去。接收机面对的却是一堆被噪声、多径污染过的复数点它需要从一堆乱七八糟的采样里猜发射端到底发了哪个符号。硬判决的做法是求最小欧氏距离。假设接收点是 r发射符号是 s_ii1..M硬判决就是计算argmin |r - s_i|^2谁离得最近就判谁。这个思路在信噪比高的时候没问题一遇到噪声边缘场景就尴尬了接收点落在两个星座点正中间附近判决器随便挑一个概率信息全扔掉。后面如果接了一个软输入译码器比如 LDPC 的置信传播或 Turbo 的 BCJR 算法它需要知道的是“这个比特是 1 的可能性有多大是 0 的可能性有多大”而不是一个孤零零的 0 或 1。软解调的核心在于把判决问题转化成概率问题。给定接收点 r发射符号为 s_i 的后验概率正比于exp(-|r - s_i|^2 / (2σ^2))其中 σ^2 是噪声方差。这个式子看着简单却是整个软比特生成的基础。每个数据比特 b_k 对应一组符号集合所有第 k 位为 0 的符号组成集合 S_k^0第 k 位为 1 的组成 S_k^1。那么这个比特的软信息就是对集合内符号的后验概率求和再取对数也就是教科书里的对数似然比LLR。LLR 的正负决定比特判 0 还是判 1绝对值大小决定这个判决的把握程度。硬判决相当于把上述概率过程简化成只找最优点、信心值永远拉满这就是它丢失信息的地方。实际工程里直接套公式的问题在于指数运算高密度出现计算量偏大。所以 qamdet.m 这类代码一般不会硬上全概率求和而是用近似或查表来生成软比特这点在后面章节会展开。2.2 LLR 的计算方法两种近似实现与参数含义最标准的 LLR 定义是L(b_k) ln( P(b_k1 | r) / P(b_k0 | r) )按全概率展开分子是 S_k^1 里所有符号的后验概率之和分母是 S_k^0 里所有符号的后验概率之和。直接求和的方法就是 Max-Log-MAP 的对数域版本工程上更常用的是近似因为指数运算具有“大数压倒小数”的特性求和可以直接用最大值代替这就是 Max-Log 近似。公式变成L(b_k) ≈ 1/σ^2 × ( min_{s∈S_k^0} |r - s|^2 - min_{s∈S_k^1} |r - s|^2 )也就是说软比特等于“离接收点最近的 0 位符号的距离”减去“离接收点最近的 1 位符号的距离”再除以噪声方差。注意符号方向如果最近的是 1 位符号距离差为正LLR 为正判成 1 可信度高反之判成 0。这个近似省掉了对数运算只求两次最小距离在 MATLAB 里可以用矩阵广播一次算完。参数上最关键的是噪声方差 σ^2。这个值如果给得不对LLR 的幅度就会失真。给大了软比特绝对值被压缩置信度偏保守给小了软比特被放大译码器会过度相信每个比特。很多仿真代码直接把 σ^2 定为 AWGN 信道下加的复噪声方差这在仿真环境里没问题在硬件实机上就得注意从接收信号里估计噪声功率。另一个参数是归一化幅度星座点坐标如果按平均能量归一化距离就变成了归一化距离和实际幅度电平无关。qamdet.m 里一般都会把归一化系数和星座点集合提前算好避免每次解调重复生成星座图。function L qamdet(r, constellation, bitmap, No) % 输入 r: 接收符号序列, Nx1 % 输入 constellation: 星座点向量, Mx1, 已经能量归一化 % 输入 bitmap: 星座点对应的比特映射表, MxK, 每一行是一个符号的 K 比特 % 输入 No: 噪声方差 % 输出 L: 软比特 LLR, NxK, 正值偏 1, 负值偏 0 N length(r); M length(constellation); K size(bitmap, 2); L zeros(N, K); for n 1:N d abs(r(n) - constellation).^2; % 到每个星座点的距离 for k 1:K idx0 find(bitmap(:,k)0); idx1 find(bitmap(:,k)1); min0 min(d(idx0)); min1 min(d(idx1)); L(n,k) (min0 - min1)/No; end end end这一段是典型的教学实现三重循环性能不行但思路非常直观。真正工程化的版本会用repmat或者reshape一次性算出所有距离矩阵再把每个比特的最小值用min函数沿维度压缩。后面讲进阶时会给出加速写法。这里要特别留意bitmap的构造方式常见的是格雷码映射相邻星座点之间只差一个比特这样软判决的错误模式不容易让两个比特同时错。3. 文件拆解与运行链路从 random_binary 到 main_plot 的完整数据流3.1 每个 .m 文件的角色调制端的四步走这套源码的文件名非常直白完整地对应了一条基带仿真链路。从发射端看random_binary.m负责生成随机二进制序列典型做法是randi([0 1], N, 1)然后把比特串分组。QAM 的阶数决定了每组的比特数16-QAM 每符号 4 比特64-QAM 每符号 6 比特。two2four.m这个文件名暗示了从两比特到四符号的映射 —— 准确说应该是把二进制比特组合转换成对应的十进制符号索引比如每 4 个比特b1 b2 b3 b4变成 0 到 15 之间的整数然后查星座表。反过来four2two.m负责在接收端把符号索引还原成比特串。qam.m是调制函数本体输入符号索引和调制阶数输出复数基带信号。它内部通常会维护一个constellation向量比如 16-QAM 的 16 个星座点然后做索引映射。这里有个细节值得注意星座点的平均值不能是零偏的实际工程里会做能量归一化让所有点的平均功率等于 1这样后续加噪声时噪声方差的定义才干净。constel.m单独抽出来画星座图它不参与数据链路只是一个可视化辅助函数。bshape.m是基带成形滤波典型实现是根升余弦滤波器因为仿真里经常假定收发两端各自用匹配滤波合成之后是完整的升余弦。实现一个 RRC 滤波器的方法很多可以直接调用rcosdesign也可以手写频域响应。接收端核心是qamdet.m也就是软解调函数。它输入接收符号序列和噪声方差输出软比特。main_plot.m把所有环节串起来从生成随机数开始一路到画出误码率曲线或者星座图是整个代码的入口。这个文件的组织方式比较传统先设置参数区然后循环信噪比每个 SNR 点跑一段蒙特卡洛仿真最后汇总画图。初看这套代码的人最容易犯的错是把main_plot.m当成一个绘图工具实际上它是主控脚本绘图只是它的最终产物之一。表文件功能速查文件名角色输入输出random_binary.m信源比特数二进制序列two2four.m比特到符号索引比特组符号索引qam.m调制映射符号索引复数基带信号bshape.m脉冲成形基带符号成形后波形constel.m星座图绘制星座点图窗句柄qamdet.m软解调接收符号、噪声方差软比特 LLRfour2two.m符号索引到比特判决符号二进制序列main_plot.m主控流程无仿真图、误码率3.2 主控脚本怎么跑参数设置与绘图输出拿到这套代码以后最顺畅的复现方式是先跑一遍main_plot.m并在跑之前把参数区过一遍。典型的主控脚本长这样% main_plot.m 示例结构 clc; clear; close all; %% 参数区 M 16; % QAM 阶数 K log2(M); % 每符号比特数 nSymbol 1e5; % 蒙特卡洛仿真符号数 EbN0_dB 0:2:12; % 比特信噪比扫描点 No 1./(10.^(EbN0_dB/10)); % 噪声方差换算 %% 发射链路 bits random_binary(nSymbol * K); symIdx two2four(bits, K); txSym qam(symIdx, M); txWave bshape(txSym); %% 多信噪比循环 ber zeros(size(EbN0_dB)); for idx 1:length(EbN0_dB) rxWave txWave sqrt(No(idx)/2) * (randn(size(txWave)) 1j*randn(size(txWave))); % 接收端匹配滤波、软解调 rxSym rxWave; % 忽略滤波器延迟简化处理 LLR qamdet(rxSym, qam_constellation(M), qam_bitmap(M), No(idx)); rxBits (LLR 0); % 硬判决仅用于误码统计 ber(idx) mean(bits ~ rxBits(:)); end %% 绘图 figure; semilogy(EbN0_dB, ber, o-); grid on; xlabel(Eb/N0 (dB)); ylabel(BER);注意第 14 行和第 15 行的噪声方差换算。这里面有个单位陷阱qamdet里用的是符号级噪声方差而画误码率曲线时用的是每比特信噪比 Eb/N0。两者差了个log2(M)因子因为一个符号承载 K 个比特。如果直接把 Eb/N0 当符号信噪比用出来的曲线会左右平移几个 dB看起来像解调算法有问题其实是单位没对齐。常见做法是No 1 / (10^(EbN0_dB/10) * log2(M))这个细节在下面的避坑章节还会专门说。跑通main_plot.m只是第一步。你拿到这套代码后最应该做的是把two2four和four2two的映射表拿出来核对一下是不是格雷码映射。非格雷码在硬判决时误码率差不了多少在软解调时会直接影响 LLR 的可靠性因为相邻星座点可能差两个比特最小距离搜索出的最优点并不能代表单个比特的置信度。检查方式很简单把所有星座点的比特映射表打出来看看相邻坐标点之间是否只差一位。4. 软解调落地实现qamdet 的代码结构、输入输出与参数调整4.1 核心函数 qamdet 的软比特生成逻辑qamdet 这个名字一看就明白它是这个资源的灵魂。前面的理论部分提到的 Max-Log 近似在工程代码里通常实现为距离差除以噪声方差只不过为了速度不会像我前面写的示例那样逐符号逐比特三个循环。常见的向量化写法是先把星座点展开成三维矩阵然后用sum或min沿指定维度操作。下面给一个 16-QAM 场景下性能与可读性平衡的实现function LLR qamdet_16qam(r, No, normFactor) % 16-QAM 软解调, 输出 LLR % r: 接收复数符号, Nx1 % No: 单边带噪声方差 % normFactor: 星座点归一化系数, 一般取 sqrt(mean(abs(C).^2)) C [-3 -1 1 3] / normFactor; [I, Q] meshgrid(C, C); const I(:) 1j*Q(:); % 格雷码映射表, 4比特顺序为 b1 b2 b3 b4 bitmap [0 0 0 0; 0 0 0 1; 0 0 1 1; 0 0 1 0; ... 0 1 0 0; 0 1 0 1; 0 1 1 1; 0 1 1 0; ... 1 1 0 0; 1 1 0 1; 1 1 1 1; 1 1 1 0; ... 1 0 0 0; 1 0 0 1; 1 0 1 1; 1 0 1 0]; N length(r); LLR zeros(N, 4); for n 1:N d abs(r(n) - const).^2; % 16个距离 d0 zeros(4,1); d1 zeros(4,1); for k 1:4 d0(k) min(d(bitmap(:,k)0)); d1(k) min(d(bitmap(:,k)1)); end LLR(n,:) (d0 - d1) / No; end end这里的关键在normFactor。如果星座点用的是 ±1、±3 这种常规坐标平均功率是(4*1 4*9)/4 10归一化系数就是sqrt(10)。const里的坐标除以了normFactor那么平均功率变成 1。这样后续加噪声时如果 No 是复噪声的方差软比特的量纲就是距离差除以方差直接送给译码器使用。读这个函数要注意两点。第一bitmap的每一行对应const的每一行顺序不能错。上面这个映射表实际上是把 16 个点按象限和从内到外排列的不是随便写的。第二d0 和 d1 的顺序与比特位置对应k1 是最高位k4 是最低位。这决定了输出矩阵LLR(n,:)中每一列代表哪个比特后面接four2two或者硬判决统计时需要严格对齐。4.2 与 qam 调制参数的联动调制阶数、噪声方差、LLR 缩放软解调不是孤立函数它的输出要直接喂给译码器。这里有个容易被忽略的工程点LLR 的尺度和译码器的内部度量的匹配。LDPC 译码器对 LLR 的幅度不敏感反正迭代过程中会归一化但 Turbo 码的 Max-Log-MAP 译码器要求 LLR 正确反映信道条件。最直接的影响来自噪声方差No的精度。在蒙特卡洛仿真中你加的噪声通常设为No/2的实部和虚部系数但如果接收端经过匹配滤波或下采样噪声功率会改变。比如bshape.m做了成形滤波匹配滤波器输出端的噪声方差就不再是发射端加的No而是乘了一个滤波器能量因子。判断 LLR 是否尺度正确的简便方法是做一次硬判决对比。把LLR的符号作为硬判决结果和传统最小欧氏距离硬判决的结果比较误码率应该完全一致。如果软解调让误码率变差了多半是距离差除以噪声方差的值出了问题或者比特映射表错位。另一个常见问题是符号能量归一化用的normFactor和 qam.m 里的不一致导致距离差整体偏大或偏小。所以拿到资源后先把qam.m里的星座点坐标提取出来和qamdet里的用同一个归一化系数而不是各自维护一套。噪声方差的实际取值也要想清楚。如果信号功率已经是 1归一化后符号信噪比 SNR_s 1 / No。而比特信噪比 Eb/N0 1 / (No * log2(M))。注意这里的 No 是符号域的单边噪声方差与通常通信教材里定义的 N0 有一倍或两倍的关系。复噪声的总方差是 No实部和虚部各占 No/2。因此仿真代码里rx tx sqrt(No/2)*(randn j*randn)是对的。很多新手在这里翻车以为sqrt(No)就是复噪声幅度结果误码率曲线在低信噪比段明显偏差高信噪比段看起来还行曲线形状不对。% 从 main_plot.m 提取的噪声生成正确示例 Es mean(abs(txSym).^2); % 符号能量, 归一化后应为1 No Es / (10^(EbN0_dB(idx)/10) * log2(M)); % 由EbN0反推No noise sqrt(No/2) * (randn(size(txSym)) 1j*randn(size(txSym))); rxSym txSym noise;这段代码里的Es是符号能量如果星座点归一化做得好这个值正好是 1那么符号信噪比就等于 1/No。把No换算成EbN0_dB时乘上log2(M)因为每个符号携带 K 个比特的能量。仿真结果出来后可以先用理论误码率公式对照一下16-QAM 在 AWGN 下的理论误码率约等于每比特信噪比函数查表如果曲线偏离 0.5 dB 以上优先检查这个换算。5. 避坑指南QAM 软解调仿真里最常见的五个翻车现场5.1 误码率曲线在低信噪比段出现地板效应现象软解调跑出来的 BER 曲线到 10 dB 以后不再下降或者下降速率明显放缓形成一段“地板”。原因绝大多数情况是发送端和接收端的成形滤波器延迟没有对齐。bshape.m如果使用根升余弦滤波必然引入固定群延迟接收端直接拿原始采样点做软解调信号相位和幅度都错位了高信噪比时这种错位成为主导误差。解决在发送端成形后记录滤波器延迟 groupDelay接收端做匹配滤波后截掉前 groupDelay 个采样点。或者干脆在仿真里省略成形滤波只在需要看频谱或带外泄漏时才启用。做误码率仿真时去掉成形滤波不影响算法的正确性反而能聚焦在解调逻辑上。5.2 LLR 符号方向反了导致译码器越纠越错现象误码率明显高于硬判决甚至接近 0.5曲线像一条水平线。原因qamdet输出的是 (d0 - d1)/No这里 d0 是最小距离对应比特为 0 的星座点d1 是比特为 1 的最小距离。如果函数里写反了或者后续译码器期望正 LLR 判 1 而代码实际输出正 LLR 判 0那么整个软信息就是反向的。更隐蔽的原因在于比特映射表的排列顺序比如把bitmap(:,k)0和1的条件写反虽然功能上还是距离差但实际对比的是错误的比特集合。解决先用一个无信道编码的仿真把软解调输出做硬判决和传统硬判决结果对比。两者误码率一致才能说明 LLR 符号没有反。更简单的方法是构造一个已知序列比如全零比特发送后用软解调输出检查每一列的均值全零发送时 LLR 应该总体为负值如果所有 LLR 均值都为正说明符号反了。5.3 噪声方差参数重复换算导致 LLR 幅度失真现象误码率曲线形状和理论曲线一致但整体向右偏移约 3 dB。原因将 Eb/N0 换算成 No 的时候有的代码写No 1 / (EbN0_lin * log2(M))有的代码写No 1 / (2 * EbN0_lin * log2(M))。复 AWGN 信道下实部和虚部噪声功率各占 No/2信号平均功率如果按归一化处理符号能量为 1那么符号信噪比 SNR_s 1/No。如果误用了双倍噪声功率信噪比会翻倍曲线右移 3 dB。解决统一约定。在 qamdet 里用符号域噪声方差 No在加噪声时用sqrt(No/2)乘复数高斯随机数。不要同时使用No 1/SNR这种简化写法。检查方法很简单信噪比设为 20 dB发送固定符号计算接收符号的实测噪声方差与理论 No 做对比。5.4 星座图看起来正常但软解调输出全是 NaN现象运行到中途报NaN或Inf误码率统计出现空值。原因No为 0 时距离差除以 0 得到 Inf符号为负时可能组合出 NaN。常见于扫描 Eb/N0 时把最后一个点设成无穷大信噪比或者噪声方差在某个循环里被赋成 0。此外如果星座点坐标里混入 NaN例如归一化系数为 0所有距离都会是 NaN。解决在 qamdet 入口处做防御性检查。如果No 0直接返回符号硬判决结果即把距离差按正负赋成 ±100 这样的大数。归一化系数用eps保护避免除零。function LLR qamdet_safe(r, const, bitmap, No) if No eps % 无噪声场景直接用距离差作为符号, 不除方差 [~, idx] min(abs(r - const).^2, [], 2); LLR bitmap(idx, :); LLR 2 * LLR - 1; % 映射到 ±1 return; end % 正常软解调流程 end5.5 高阶 QAM 下用矩阵广播导致内存爆炸现象跑 64-QAM 或 256-QAM 时代码直接卡死或内存溢出。原因有些向量化写法会把距离矩阵构建成N x M x K的完整张量。N 是符号数 1e5M 是 64K 是 6那么中间矩阵大小是 1e5 * 64 * 6 * 8 字节约 3 GB直接爆内存。16-QAM 时这个矩阵小没暴露问题换成高阶就翻车。解决不要一口气展开所有维度改为对每个比特位置单独计算距离差。或者分块处理每 1000 个符号一批算软比特。实践中我用的是在比特维度上循环这样每次只产生N x M的距离矩阵内存压力小一个数量级。% 分块软解调, 适合高阶 QAM function LLR qamdet_blocked(r, const, bitmap, No, blockSize) N length(r); K size(bitmap, 2); LLR zeros(N, K); for startIdx 1:blockSize:N blockRange startIdx:min(startIdxblockSize-1, N); rBlock r(blockRange); d abs(rBlock - const.).^2; % blockSize x M for k 1:K d0 min(d(:, bitmap(:,k)0), [], 2); d1 min(d(:, bitmap(:,k)1), [], 2); LLR(blockRange, k) (d0 - d1) / No; end end endblockSize一般取 1000 到 5000具体看内存情况。这个写法在 256-QAM、1e6 个符号下也能顺畅跑完。6. 验证与进阶用误码率曲线证明软解调的价值再往高阶 QAM 扩展6.1 误码率对比脚本软判决比硬判决好多少单独跑软解调看不出优势必须和硬判决对比。最简单的验证方法是把软解调的 LLR 做符号判决得到误码率再与最小欧氏距离硬判决的误码率放在同一张图上。两者应该完全重合——因为它们本质上做的是同一个硬判决决策。真正的优势体现在级联信道编码之后这个资源虽然没有附带 LDPC 或 Turbo 译码器但你可以用 MATLAB 的comm.LDPCDecoder接在后面。% 对比硬判决和软判决级联 LDPC 的误码率 ldpcEncoder comm.LDPCEncoder; ldpcDecoder comm.LDPCDecoder; for idx 1:length(EbN0_dB) % 生成数据、编码、调制 data randi([0 1], ldpcEncoder.BlockLength, 1); encData ldpcEncoder(data); % ... 映射到 QAM 符号, 过信道 ... % 硬判决解调 hardBits qam_hard_demod(rxSym); decHard ldpcDecoder(double(hardBits)); berHard(idx) mean(data ~ decHard); % 软解调 llrOut qamdet(rxSym, const, bitmap, No(idx)); decSoft ldpcDecoder(llrOut); berSoft(idx) mean(data ~ decSoft); end跑完你会发现硬判决情况下 LDPC 几乎不工作误码率相对无编码只改善零点几个 dB软解调则让误码曲线迅速下坠在 1e-4 处可能带来 2~3 dB 增益。这个对比做完软解调的价值就很直观了。6.2 从 16-QAM 到 64/256-QAM扩展时的关键改动资源里的 qamdet 很可能是针对 16-QAM 写的要扩展到 64-QAM需要改三处星座点坐标、比特映射表、噪声归一化系数。64-QAM 的坐标是 ±1, ±3, ±5, ±7 的笛卡尔积共 64 个点每符号 6 比特。最快捷的做法是写一个通用的星座生成函数function [C, bitmap] qam_constellation(M) % M 必须是 2 的整数次幂, 且开方后为整数 sqrtM sqrt(M); levels -(sqrtM - 1):2:(sqrtM - 1); [I, Q] meshgrid(levels); C I(:) 1j * Q(:); C C / sqrt(mean(abs(C).^2)); % 能量归一化 % 格雷码映射生成bitmap gray bitxor(0:sqrtM-1, bitshift(0:sqrtM-1, -1)); % 按行和列组合成 K 位格雷码 end扩展后可以顺手把qamdet里的距离计算改成分块 比特循环的版本就是 5.4 里那个qamdet_blocked。我在拿到这类 QAM 资源后习惯直接把调制和解调都改成参数化的M 作为输入传来传去而不是写死 16。这样以后跑 64-QAM 只需改一个数。有一说一改成通用版本的过程也是检查原资源逻辑是否严谨的过程星座点归一化、比特映射顺序、噪声方差换算每个环节都重新核对一遍比直接拿默认参数跑要有把握得多。从那以后我每拿到一个通信仿真资源都强制自己先做一次“硬判决一致性测试”——先不带信道编码跑软解调和硬判决对比误码率确认一致后才开始接 LDPC 或 Turbo 码。这个习惯帮我挡掉了至少三次因为映射表顺序错位导致的返工希望也能帮到你。本文还有配套的精品资源点击获取
返回列表