简介:这份资源面向数字通信与无线通信方向的学习者与工程实践者,聚焦调制解调链路中的关键环节,帮助理解从信号映射到比特恢复的完整流程。包内共2个文件,均为m脚本文件,分别对应16-QAM调制与解调的实现,压缩包整体约1KB,体量轻便,便于直接阅读与二次修改。内容围绕16-QAM星座映射、相干检测与软解调展开,并可与极化码等信道编码方案衔接,用于分析软信息在错误纠正中的价值,适合作为课程实验、算法验证或通信系统仿真的参考素材。目前已有163人学习下载,说明该主题在相关学习群体中具有一定关注度。读者可借助脚本梳理调制与解调的核心步骤,理解幅度相位联合映射、匹配滤波与概率估计之间的关系,并在此基础上扩展至更高阶QAM或结合极化码进行链路级性能评估,为后续系统设计与优化提供可复用的代码基础。
1. 从 QAM 软解调值不值得做:极化码链路里最容易被低估的一环
做无线通信基带的人,迟早会撞上这样一个组合:极化码(Polar Code)做信道编码,QAM 做高阶调制,中间夹一层软解调。标题里这一串关键词——调制与解调、极化码、QAM 解调、软解调——说的就是这条链路。它解决的核心问题是:在 16QAM、64QAM 这类高阶调制下,怎么把接收符号里那点“可信度”完整地喂给极化码译码器,而不是粗暴地硬判决成 0/1 再丢进去。适合谁看?正在做链路级仿真、基带算法验证,或者被“硬解调性能上不去”卡住的工程师。软解调不是玄学,它决定了你的极化码能拿到多少编码增益,做对了能省下好几个 dB 的发射功率,做错了就是白忙一场。
2. 极化码 + QAM 软解调:先搞清楚 LLR 到底在算什么
2.1 硬判决丢掉了什么,软解调补回了什么
先把这个链路的数据流捋一遍。发送端:信息比特 → 极化码编码 → 比特交织 → QAM 星座映射 → 上变频发射。接收端反过来:下变频 → QAM 符号 → 软解调输出 LLR → 极化码译码 → 信息比特。
关键就在“QAM 符号 → LLR”这一步。硬解调只输出 0 或 1,等于把接收符号里“我有多确定它是 0”这个信息全扔了。软解调输出的是对数似然比:
LLR(b) = ln[ P(b=0 | y) / P(b=1 | y) ]LLR 的绝对值越大,说明这个比特越确定;符号表示偏向哪一边。极化码的 SC(Successive Cancellation)或 SCL 译码器吃的就是这个软信息。你给它硬判决,它就只能当每个比特同等可信,编码增益直接打折。
常见做法是:对每个接收符号 y,遍历星座图上所有点,算欧氏距离,再按比特分组做 log-sum-exp 近似。这就是所谓的 max-log 近似,工程上最常用,因为复杂度可控。
2.2 16QAM 软解调的完整推导与实现
16QAM 星座图是 4×4 的方格,每个符号携带 4 个比特。假设采用格雷映射(Gray Mapping),I 路和 Q 路各 2 个比特,可以独立处理。这是工程上最省事的做法,也是我一般会推荐的第一步。
设接收符号为 y = (y_I, y_Q),星座点间距为 2d(即相邻点坐标差 2d),则 I 路两个比特 b0、b1 的 LLR 可以写成:
import numpy as np def qam16_soft_demod(y_i, y_q, d=1.0, noise_var=1.0): """ 16QAM 软解调,max-log 近似,格雷映射 y_i, y_q: 接收符号的 I/Q 分量(已做信道均衡) d: 星座点半间距,标准 16QAM 归一化后 d = 1/sqrt(10) noise_var: 噪声方差 sigma^2 返回: 长度为 4 的 LLR 数组,顺序 [b0, b1, b2, b3] """ # I 路:b0 决定符号,b1 决定幅度 # 格雷映射下,b0 的判决边界在 y_i = 0 llr_b0 = 4 * d * y_i / noise_var # b1 的判决边界在 |y_i| = 2d llr_b1 = 4 * d * (2 * d - np.abs(y_i)) / noise_var # Q 路同理 llr_b2 = 4 * d * y_q / noise_var llr_b3 = 4 * d * (2 * d - np.abs(y_q)) / noise_var return np.array([llr_b0, llr_b1, llr_b2, llr_b3])这段代码的逻辑说明:max-log 近似下,LLR 可以化简成“到最近 0 点距离平方减去到最近 1 点距离平方”,再除以噪声方差。对于格雷映射的 16QAM,I 路和 Q 路完全解耦,b0 只看 y_i 的符号,b1 只看 y_i 的绝对值离 2d 有多远。参数 d 是星座点半间距,noise_var 是噪声方差,这两个值必须和实际链路匹配,否则 LLR 的尺度会错,译码器性能会明显下降。
注意:noise_var 的估计精度直接影响软解调效果。工程上一般用导频或前导码做信噪比估计,再换算成噪声方差。如果噪声方差估大了,LLR 整体偏小,译码器会“不信任”软信息;估小了,LLR 偏大,容易过自信导致译码错误。
2.3 64QAM 与高阶调制的扩展思路
64QAM 是 8×8 星座,每符号 6 比特。I 路 3 比特,Q 路 3 比特。格雷映射下仍然可以按路独立处理,但每个比特的判决边界不再是简单的 0 和 2d,而是 0、2d、4d 三个边界。
我一般会用一个通用公式来写:
def qam64_soft_demod_axis(y, d=1.0, noise_var=1.0): """ 64QAM 单路(I 或 Q)软解调,3 比特,格雷映射 y: 单路接收分量 返回: 3 个 LLR """ llr = np.zeros(3) # b0: 判决边界 0 llr[0] = 4 * d * y / noise_var # b1: 判决边界 4d 和 -4d,取最近边界 llr[1] = 4 * d * (4 * d - np.abs(y)) / noise_var # b2: 判决边界 2d 和 6d,需要分段 if np.abs(y) < 2 * d: llr[2] = 4 * d * (2 * d - np.abs(y)) / noise_var elif np.abs(y) < 6 * d: llr[2] = 4 * d * (np.abs(y) - 4 * d) / noise_var else: llr[2] = 4 * d * (6 * d - np.abs(y)) / noise_var return llr参数说明:d 的取值取决于归一化方式。如果星座点坐标是 ±1d、±3d、±5d、±7d,那么平均功率是 (1+9+25+49)/4 × 2d² = 42d²,归一化到 1 需要 d = 1/sqrt(42)。这个 d 必须和发射端一致,否则 LLR 尺度全错。
对于更高阶的 256QAM,思路一样,只是分段更多。工程上更推荐用查表法:预先算好每个星座点的距离,接收时直接查表加比较,比实时算公式更稳。
3. 把软解调接进极化码译码器:LLR 怎么喂、参数怎么调
3.1 极化码译码器对 LLR 的尺度要求
极化码的 SC 译码核心是 f 函数和 g 函数:
f(a, b) = sign(a) * sign(b) * min(|a|, |b|) g(a, b, u) = b + (1 - 2u) * a这两个函数对 LLR 的绝对尺度不敏感,但对相对尺度敏感。也就是说,如果你把所有 LLR 同比例放大或缩小,译码结果不变。但如果你对某些比特的 LLR 估得偏大、另一些偏小,译码器就会做出错误判断。
所以软解调输出的 LLR 必须保持一致的尺度。这就是为什么 noise_var 估计这么重要——它决定了所有 LLR 的全局缩放因子。工程上我一般会在软解调之后加一步 LLR 归一化,把 LLR 的方差归一化到 1 附近,这样译码器不用关心前端增益变化。
def normalize_llr(llr, target_var=1.0): """ 将 LLR 序列归一化到目标方差 用于消除前端增益波动对译码器的影响 """ current_var = np.var(llr) if current_var < 1e-10: return llr scale = np.sqrt(target_var / current_var) return llr * scale这个归一化步骤在链路级仿真里经常被忽略,但在实际硬件实现中非常关键。因为 AGC(自动增益控制)会引入增益波动,如果不归一化,译码器看到的 LLR 尺度会随信号强度变化,导致性能不稳定。
3.2 从仿真到定点:LLR 量化位宽怎么选
浮点仿真跑通之后,下一步就是定点化。LLR 的量化位宽直接影响硬件资源和译码性能。我一般会按这个流程走:
第一步,统计浮点 LLR 的分布。在典型信噪比下跑一段数据,看 LLR 的绝对值范围。16QAM 在 SNR=10dB 时,LLR 绝对值一般在 0 到 20 之间。
第二步,选量化格式。常用的是 6 比特或 8 比特有符号数,其中 1 位符号位,其余整数位。6 比特能表示 -32 到 31,对大多数场景够用。
第三步,做饱和处理。超过量化范围的 LLR 直接截断到最大值,不要溢出。
def quantize_llr(llr, bit_width=6): """ LLR 定点量化,有符号,饱和截断 bit_width: 总位宽,含符号位 """ max_val = (1 << (bit_width - 1)) - 1 min_val = -(1 << (bit_width - 1)) quantized = np.clip(np.round(llr), min_val, max_val) return quantized.astype(np.int32)参数说明:bit_width 越大,量化误差越小,但硬件资源消耗越大。工程上 6 比特是性能和面积的平衡点,8 比特用于对性能要求极高的场景。注意饱和截断的方向:正数截到 max_val,负数截到 min_val,不要搞反。
3.3 链路级仿真的最小验证框架
要验证软解调 + 极化码这条链路,最小仿真框架需要包含:极化码编码、QAM 映射、AWGN 信道、软解调、极化码译码、误码率统计。
def simulate_polar_qam16(snr_db, num_blocks=1000, n=256, k=128): """ 极化码 + 16QAM 软解调链路级仿真 snr_db: 信噪比 num_blocks: 仿真块数 n: 极化码码长 k: 信息比特数 """ snr_linear = 10 ** (snr_db / 10.0) noise_var = 1.0 / (2 * snr_linear) # 复数噪声方差 d = 1.0 / np.sqrt(10) # 16QAM 归一化半间距 bit_errors = 0 total_bits = 0 for _ in range(num_blocks): info_bits = np.random.randint(0, 2, k) # 极化码编码(此处用简化占位,实际需完整编码器) coded_bits = polar_encode(info_bits, n) # 16QAM 映射 symbols = qam16_map(coded_bits, d) # AWGN noise = np.sqrt(noise_var / 2) * ( np.random.randn(len(symbols)) + 1j * np.random.randn(len(symbols)) ) received = symbols + noise # 软解调 llr = [] for s in received: llr.extend(qam16_soft_demod(s.real, s.imag, d, noise_var)) llr = np.array(llr) # 极化码译码(SC 或 SCL) decoded = polar_decode(llr, n, k) bit_errors += np.sum(decoded != info_bits) total_bits += k ber = bit_errors / total_bits return ber这个框架的关键点:noise_var 的计算要和 QAM 归一化方式匹配。如果星座点平均功率是 1,那么复数噪声方差就是 1/(2×SNR_linear)。d 的取值也要和映射函数一致。仿真时先跑高 SNR 确认无误码,再逐步降低 SNR 看瀑布曲线。
4. 软解调落地时最容易翻车的几个地方
4.1 现象:译码器性能比硬判决还差
原因:LLR 符号搞反了。软解调输出的 LLR 定义是 ln[P(b=0)/P(b=1)],如果译码器期望的是 ln[P(b=1)/P(b=0)],那所有软信息都是反的,译码器会系统性地往错误方向走。
解决:在链路里加一个已知序列测试。发全 0,看软解调输出的 LLR 是不是全正;发全 1,看是不是全负。如果不是,把 LLR 取反。
4.2 现象:低 SNR 下性能急剧恶化,高 SNR 下正常
原因:LLR 饱和截断太狠。低 SNR 时 LLR 绝对值本来就小,如果量化位宽不够,很多小 LLR 被截断成 0,译码器丢失了软信息。
解决:检查量化位宽,低 SNR 下 LLR 动态范围更大,需要更多比特。或者改用浮点仿真确认是量化问题还是算法问题。
4.3 现象:不同 SNR 下性能波动大,曲线不平滑
原因:噪声方差估计不准。如果 noise_var 用固定值,而实际噪声功率随 SNR 变化,LLR 尺度就会错。
解决:每个 SNR 点单独估计噪声方差,或者用信噪比估计模块实时输出。仿真时直接用真实噪声方差,硬件实现时用导频估计。
4.4 现象:64QAM 比 16QAM 性能差很多,超出理论预期
原因:格雷映射没做对。64QAM 的格雷映射比 16QAM 复杂,如果映射表错了,相邻星座点之间会有多个比特翻转,软解调出来的 LLR 质量会严重下降。
解决:用标准格雷映射表,或者用计算机搜索验证:相邻星座点的比特差异应该只有 1 位。
4.5 现象:极化码译码器偶尔崩溃,输出全 0 或全 1
原因:LLR 里出现了 NaN 或 Inf。通常是 noise_var 设成了 0,或者 log-sum-exp 计算时出现了数值溢出。
解决:在软解调输出加保护,noise_var 下限设为 1e-6,LLR 做 clip 到 [-100, 100]。
5. 进阶技巧:用 LLR 分布反推软解调是否健康
跑完仿真别只看 BER 曲线,我习惯再画一张 LLR 分布图。具体做法:在固定 SNR 下,把软解调输出的 LLR 按“发送比特为 0”和“发送比特为 1”分成两组,分别画直方图。
健康的软解调,两组 LLR 应该分得很开:发送 0 的那组集中在正半轴,发送 1 的那组集中在负半轴,中间重叠区域越小越好。如果两组几乎重叠,说明软解调没提供有效信息,译码器再强也救不回来。
更进一步,可以算一下 LLR 的互信息:
def llr_mutual_information(llr, tx_bits): """ 计算 LLR 与发送比特之间的互信息 用于评估软解调质量 """ # 将 LLR 分桶,近似计算条件概率 bins = np.linspace(-20, 20, 50) mi = 0.0 for b in [0, 1]: mask = tx_bits == b hist, _ = np.histogram(llr[mask], bins=bins, density=True) p = hist * (bins[1] - bins[0]) p = p / np.sum(p) for i, pi in enumerate(p): if pi > 0: mi += 0.5 * pi * np.log2(2 * pi / (p[i] + 1e-10)) return mi这个互信息值在 0 到 1 之间,越接近 1 说明软解调质量越好。我一般会在不同 SNR 下扫一遍,看互信息随 SNR 的增长曲线。如果曲线在某个 SNR 点突然变平,说明软解调在这个区间出了问题,通常是量化或噪声估计的锅。
还有一个习惯:每次改完软解调参数,先跑一遍“无噪声”测试。把 noise_var 设成极小值,看译码器能不能 100% 正确译码。如果无噪声都译不对,那肯定是 LLR 符号或映射表的问题,不用往下查了。这个习惯帮我省过很多次 debug 时间,算是血泪经验。
希望帮到你。
本文还有配套的精品资源,点击获取