简介:这份资源是面向通信工程、无线通信方向学生与研究人员的一套LDPC编码与64-QAM调制联合仿真程序,用于在非理想信道条件下分析误码率与抗干扰性能,帮助理解纠错编码与高阶调制如何协同提升频谱效率与传输可靠性。压缩包共8个文件,约13KB,以m脚本文件为主,辅以mat数据文件和一份txt说明,涵盖编码、调制、信道与解码等环节,便于直接运行与二次修改。内容预览显示其中包含校验矩阵数据、64-QAM调制解调脚本以及基于软判决的LDPC解码实现,可完整走通从编码到译码的仿真流程,适合用来验证消息传递类译码算法、观察不同信噪比下的性能曲线。目前已有305人学习,对需要动手复现LDPC与64-QAM交互过程、优化通信链路参数的读者具有参考价值。
1. LDPC 与 64QAM 联合仿真:从标题拆解到可复现链路
如果你手头只有一个LDPC_64QAM.rar这样的压缩包名字,或者正在搜「LDPC 64QAM 联合仿真怎么做」,大概率你面对的是这样一个场景:通信系统课程设计、毕设开题,或者公司里要快速验证一套高阶调制加信道编码的链路性能。标题里的三个关键词——LDPC、64QAM、QAM 符号——其实指向一条完整的物理层链路:信源比特经过 LDPC 编码,再映射成 64QAM 符号,过信道,接收端做软解调、LDPC 译码,最后统计误码率。这套东西在 5G NR 的数据信道、DVB-S2X、部分光通信标准里都是核心组合,不是玩具。
它适合谁?适合已经懂一点 MATLAB 或 Python、知道 BPSK 和 QPSK 怎么回事、但没亲手把 LDPC 和 64QAM 接起来跑通过的人。也适合那些跑过现成脚本但 BER 曲线怎么都不对、怀疑是解调软信息给错的人。这篇笔记按「先立住原理、再动手复现、最后说坑」的顺序走,中间给可直接抄的代码和参数,不堆公式,重点放在能跑通、能解释现象上。
2. 为什么是 LDPC 加 64QAM:选型逻辑与链路拆解
2.1 这对组合到底解决了什么问题
单独看 64QAM,每个符号带 6 个比特,频谱效率是 QPSK 的 3 倍,但星座点间距小,噪声容限差,不加编码时误码率很难看。单独看 LDPC,它是一类稀疏校验矩阵定义的线性分组码,译码用置信传播,能逼近香农限,而且码长码率灵活。把两者接起来,逻辑是:64QAM 负责把频谱效率拉上去,LDPC 负责把可靠性补回来。接收端不是硬判出 0/1 再译码,而是把每个比特的软信息(LLR,对数似然比)送给 LDPC 译码器,译码器再迭代修正。软信息质量直接决定译码增益,这也是后面最容易翻车的地方。
常见做法是:LDPC 码率选 1/2 或 2/3,码长几千到几万比特,64QAM 采用格雷映射。格雷映射的意义是相邻星座点只差一个比特,这样某个符号判错时,大概率只错一个比特,LLR 不会整体崩掉。如果你用的映射表不是格雷的,BER 曲线在中等信噪比会明显翘尾,这是血泪经验。
2.2 链路每一级的输入输出要盯死
一条最小可复现链路长这样:
- 信源:随机比特,长度 = 码长 × 码率,且要能被调制阶数整除。
- LDPC 编码:用稀疏校验矩阵 H 生成码字,输出码长 N。
- 比特交织(可选但建议):打散连续比特,避免深衰落或星座点相关性导致连续错误。
- 64QAM 映射:每 6 个比特映射成一个复数符号。
- 信道:AWGN 最基础,先跑通再加瑞利。
- 软解调:对每个比特算 LLR,注意是「每比特」不是「每符号」。
- LDPC 译码:置信传播迭代,输出硬判比特。
- 误码统计:和原始信源比,算 BER。
这里每一步的维度都要对得上。我一般会在代码里强制断言:编码输出长度等于 N,映射前比特数等于符号数乘 6,解调输出 LLR 长度等于 N。维度错位是新手最常见的翻车点,而且报错往往出现在译码器内部,看起来像算法问题,其实是数据没对齐。
2.3 参数怎么定:码长、码率、迭代次数
| 参数 | 常用取值 | 影响 |
|---|---|---|
| LDPC 码长 N | 648、1296、1944、6480 | 码长越长增益越好,但译码耗时上升 |
| 码率 R | 1/2、2/3、3/4 | 码率越低冗余越多,抗噪越好,频谱效率下降 |
| 译码迭代次数 | 20~50 | 次数不够译码不收敛,次数过多收益递减 |
| 64QAM 映射 | 格雷映射 | 非格雷映射会导致 LLR 质量下降 |
| 信噪比范围 | 8~20 dB(Es/N0) | 低于 8 dB 基本译不出来,高于 20 dB 无意义 |
注意信噪比的定义。仿真里经常混用 Eb/N0 和 Es/N0,64QAM 下两者差10*log10(6)约 7.78 dB。如果你看别人的曲线在 12 dB 就瀑布下降,自己跑到 20 dB 才降,先检查是不是单位搞混了。这个坑我踩过不止一次。
3. 用 Python 把 LDPC 加 64QAM 链路跑起来
3.1 环境与依赖:最小可用集合
不依赖任何通信专用库也能跑,核心只需要 numpy。LDPC 的校验矩阵可以自己构造一个规则码,或者用现成的稀疏矩阵生成方式。下面这套代码是教学向的最小实现,重点在链路结构清晰,不追求工业级译码速度。
# 建议 Python 3.9 以上,只装 numpy 即可 pip install numpy如果你要用现成的 LDPC 库,常见做法是找支持 5G NR LDPC 的包,但那些包接口复杂,初学阶段反而容易迷失。我一般先用自己写的规则 LDPC 跑通,再换标准码。
3.2 构造一个可用的 LDPC 校验矩阵
规则 LDPC 的构造方式很多,这里用最直观的 Gallager 随机构造思路:固定每列 1 的个数和每行 1 的个数,随机打散。虽然性能不如优化过的码,但足够验证链路。
import numpy as np def build_ldpc_matrix(n, dv=3, dc=6, seed=42): """ 构造规则 LDPC 校验矩阵 H,尺寸 (m, n) dv: 每列 1 的个数(列重) dc: 每行 1 的个数(行重) 要求 m = n * dv / dc 为整数 """ rng = np.random.default_rng(seed) m = n * dv // dc assert m * dc == n * dv, "参数不满足规则码行列重约束" H = np.zeros((m, n), dtype=int) # 按块构造:每块行内 1 的位置循环移位,保证列重均匀 block_rows = m // dv for i in range(dv): for j in range(block_rows): row = i * block_rows + j cols = [(j * dc + k) % n for k in range(dc)] H[row, cols] = 1 # 随机列置换,打散结构 perm = rng.permutation(n) H = H[:, perm] return H H = build_ldpc_matrix(648, dv=3, dc=6) print("H shape:", H.shape, "行重:", H.sum(axis=1)[:5], "列重:", H.sum(axis=0)[:5])这段代码的逻辑说明:先按块保证每行有 dc 个 1,再通过随机列置换打散,避免规则结构导致译码性能差。参数 dv 和 dc 决定码率,码率 R ≈ 1 - dv/dc,这里 dv=3、dc=6 对应 R=1/2。注意m * dc == n * dv这个断言,不满足就说明你选的参数构不成规则码,得换。
3.3 编码:从校验矩阵到码字
有了 H,编码可以用高斯消元把 H 化成系统形式,也可以直接用稀疏矩阵求解。教学场景下用高斯消元最直观。
def ldpc_encode(H, msg_bits): """ 系统 LDPC 编码:H = [A | B],码字 = [msg | parity] 通过高斯消元把 H 转为 [I | P] 形式求校验位 """ m, n = H.shape k = n - m assert len(msg_bits) == k, f"消息长度应为 {k}" # 高斯消元求校验部分(简化实现,仅适用于小规模) H_mod = H.copy() % 2 # 对前 m 列做消元,目标是得到单位阵 for col in range(m): pivot = None for r in range(col, m): if H_mod[r, col] == 1: pivot = r break if pivot is None: raise ValueError("矩阵不可逆,换一组参数或种子") H_mod[[col, pivot]] = H_mod[[pivot, col]] for r in range(m): if r != col and H_mod[r, col] == 1: H_mod[r] = (H_mod[r] + H_mod[col]) % 2 P = H_mod[:, m:] # 校验部分 parity = (P @ msg_bits) % 2 return np.concatenate([msg_bits, parity]).astype(int) msg = np.random.randint(0, 2, 324) codeword = ldpc_encode(H, msg) print("码字长度:", len(codeword), "校验通过:", np.all((H @ codeword) % 2 == 0))逻辑说明:高斯消元把 H 的前 m 列变成单位阵,剩下的部分就是校验生成矩阵。最后一行断言H @ codeword % 2 == 0是必须的,如果不过,说明消元过程有误,后面所有仿真都白做。参数上,消息长度 k = n - m,这里 n=648、m=324,所以 k=324。
3.4 64QAM 格雷映射与软解调
映射和软解调是整条链路最需要小心的地方。64QAM 星座图有 64 个点,每个点对应 6 个比特。格雷映射下,相邻点的比特标签只差一位。
def qam64_gray_map(bits): """ 64QAM 格雷映射:每 6 比特 -> 一个复数符号 采用常见的 I/Q 各 8 电平,格雷编码 """ bits = bits.reshape(-1, 6) # 格雷码到电平的映射表(3 比特 -> 8 电平) gray_to_level = np.array([-7, -5, -3, -1, 1, 3, 5, 7]) # 3 比特格雷码转二进制索引 def gray2bin(g): b = g.copy() for i in range(1, len(g)): b[i] = b[i] ^ b[i-1] return b symbols = [] for row in bits: i_bits = row[:3] q_bits = row[3:] i_idx = gray2bin(i_bits) q_idx = gray2bin(q_bits) i_val = gray_to_level[int(''.join(map(str, i_idx)), 2)] q_val = gray_to_level[int(''.join(map(str, q_idx)), 2)] symbols.append((i_val + 1j * q_val) / np.sqrt(42)) return np.array(symbols) def qam64_soft_demod(rx_symbols, noise_var): """ 64QAM 软解调:对每个比特输出 LLR LLR = log( P(b=0|y) / P(b=1|y) ) 用 max-log 近似简化 """ gray_to_level = np.array([-7, -5, -3, -1, 1, 3, 5, 7]) levels = gray_to_level / np.sqrt(42) llrs = [] for y in rx_symbols: # 遍历所有 64 个星座点,找每个比特的最优 for bit_pos in range(6): d0, d1 = [], [] for i_idx in range(8): for q_idx in range(8): # 由电平反推格雷比特 point = levels[i_idx] + 1j * levels[q_idx] dist = np.abs(y - point) ** 2 # 这里简化:直接用电平索引的二进制位作为比特 bit_val = (i_idx >> (2 - bit_pos)) & 1 if bit_pos < 3 else (q_idx >> (5 - bit_pos)) & 1 if bit_val == 0: d0.append(dist) else: d1.append(dist) llr = (min(d1) - min(d0)) / (2 * noise_var) llrs.append(llr) return np.array(llrs)逻辑说明:映射部分把 6 比特拆成 I 路 3 比特、Q 路 3 比特,各自做格雷到二进制的转换再查电平表。归一化除以sqrt(42)是为了让平均符号功率为 1,42 是 64QAM 各电平平方的均值。软解调用 max-log 近似,对每个比特找最近的距离,LLR 的符号表示倾向,幅度表示置信度。参数noise_var必须和信道加噪的方差一致,否则 LLR 尺度不对,译码器迭代会失效。
3.5 置信传播译码与 BER 统计
译码器用标准的和积算法,迭代更新变量节点和校验节点的消息。
def ldpc_decode(H, llr, max_iter=30): """ 置信传播译码(和积算法,对数域) H: 校验矩阵 llr: 信道软信息 """ m, n = H.shape # 初始化变量节点到校验节点的消息 L = np.tile(llr, (m, 1)) * H for it in range(max_iter): # 校验节点更新 L_check = np.zeros_like(L) for r in range(m): cols = np.where(H[r] == 1)[0] for c in cols: others = [x for x in cols if x != c] prod = 1.0 for o in others: prod *= np.tanh(L[r, o] / 2) prod = np.clip(prod, -1 + 1e-12, 1 - 1e-12) L_check[r, c] = 2 * np.arctanh(prod) # 变量节点更新 L_total = llr + L_check.sum(axis=0) # 硬判 hard = (L_total < 0).astype(int) if np.all((H @ hard) % 2 == 0): return hard, it # 更新消息 for r in range(m): cols = np.where(H[r] == 1)[0] for c in cols: L[r, c] = L_total[c] - L_check[r, c] return hard, max_iter def run_simulation(ebn0_db, n=648, dv=3, dc=6, trials=20): H = build_ldpc_matrix(n, dv, dc) k = n - H.shape[0] ber_total, bit_total = 0, 0 for _ in range(trials): msg = np.random.randint(0, 2, k) cw = ldpc_encode(H, msg) # 补齐到 6 的倍数 pad = (-len(cw)) % 6 cw_pad = np.concatenate([cw, np.zeros(pad, dtype=int)]) tx = qam64_gray_map(cw_pad) # Eb/N0 转 Es/N0,64QAM 每符号 6 比特,码率 R R = k / n esn0_db = ebn0_db + 10 * np.log10(6 * R) noise_var = 10 ** (-esn0_db / 10) noise = np.sqrt(noise_var / 2) * (np.random.randn(len(tx)) + 1j * np.random.randn(len(tx))) rx = tx + noise llr = qam64_soft_demod(rx, noise_var) llr = llr[:len(cw)] hard, _ = ldpc_decode(H, llr) ber_total += np.sum(hard != cw) bit_total += len(cw) return ber_total / bit_total for ebn0 in [4, 6, 8, 10]: ber = run_simulation(ebn0) print(f"Eb/N0={ebn0} dB, BER={ber:.2e}")逻辑说明:校验节点更新用 tanh 规则,变量节点更新把信道 LLR 和校验消息相加。硬判后检查是否满足所有校验方程,满足就提前退出。仿真里 Eb/N0 转 Es/N0 的公式是Eb/N0 + 10*log10(6*R),这一步错了曲线整体平移。参数max_iter=30是折中,迭代到 30 次还不收敛基本就是信噪比不够。跑出来的 BER 应该随 Eb/N0 单调下降,如果出现平台或反弹,先查 LLR 尺度和噪声方差是否匹配。
4. 避坑与排查:LDPC 64QAM 仿真里最容易翻车的五件事
4.1 BER 曲线不下降,一直停在 0.5 附近
现象:无论信噪比调到多高,误码率都在 0.5 左右,像没编码一样。原因通常是 LDPC 译码器根本没工作,或者 LLR 符号反了。LLR 定义是log(P(0)/P(1)),如果软解调输出的是log(P(1)/P(0)),译码器会把所有比特判反,BER 恒为 0.5。解决:拿一个无噪声符号过一遍软解调,看对应比特的 LLR 符号是否和原始比特一致,不一致就把 LLR 整体取负。
4.2 低信噪比下译码迭代不收敛,高信噪比突然变好
现象:Eb/N0 低于某个值时 BER 几乎不降,过了阈值突然瀑布下降。这本身是 LDPC 的正常阈值行为,但如果阈值比理论值高很多,就要查噪声方差。常见错误是加噪时用了noise_var但软解调里传了noise_var/2,或者 Es/N0 和 Eb/N0 换算漏了码率。解决:在加噪后统计实际符号误差功率,和设定值对比,偏差超过 10% 就说明换算有问题。
4.3 64QAM 映射后符号功率不归一化
现象:BER 曲线整体偏移,信噪比定义和实际不符。原因:64QAM 电平是 ±1、±3、±5、±7,平均功率是 42,不归一化的话实际发射功率远大于 1,等效信噪比比设定值高。解决:映射后统一除以sqrt(42),并在文档里写清楚归一化方式。如果用了其他电平表,重新算平均功率。
4.4 比特补齐和截断导致维度错位
现象:译码器报数组维度错误,或者 BER 统计时长度对不上。原因:LDPC 码长不一定能被 6 整除,映射前补零、解调后截断,如果补零位置和截断位置不一致,LLR 和码字就错位了。解决:在编码后记录原始码长,补齐只补在尾部,解调后严格按原始码长截断。我一般会在代码里加断言len(llr_cut) == len(codeword)。
4.5 迭代次数设太小,误以为算法不行
现象:高信噪比下 BER 还是比预期高一个数量级。原因:max_iter设成 5 或 10,LDPC 还没收敛就退出了。解决:先把迭代次数拉到 50 看性能上限,再逐步降到 20~30 找折中。注意迭代次数增加带来的耗时是线性的,仿真大规模 BER 曲线时要权衡。
5. 进阶技巧:用 EXIT 图快速判断 LDPC 与 64QAM 是否匹配
跑完整 BER 曲线很耗时,尤其码长上万、迭代几十次的时候。我一般先用 EXIT 图(外信息转移图)做快速筛查:分别画 64QAM 软解调的互信息转移曲线和 LDPC 译码器的转移曲线,两条曲线之间有开口就说明这套组合能收敛,开口越大收敛越快。具体做法是扫一组先验互信息,测软解调输出的后验互信息,再扫 LDPC 译码器的输入输出互信息。
def measure_mutual_info_awgn(snr_db, n_samples=20000): """ 测量 64QAM 软解调在给定信噪比下的互信息 简化实现:用直方图估计 """ bits = np.random.randint(0, 2, n_samples * 6) tx = qam64_gray_map(bits) esn0 = 10 ** (snr_db / 10) noise_var = 1 / esn0 noise = np.sqrt(noise_var / 2) * (np.random.randn(len(tx)) + 1j * np.random.randn(len(tx))) rx = tx + noise llr = qam64_soft_demod(rx, noise_var) # 用 LLR 和真实比特估计互信息(简化) # 实际工程中会用直方图法或高斯近似 return np.mean(np.abs(llr)) # 这里仅示意,真实互信息需分桶估计上面这段只是示意接口,真实互信息估计要对 LLR 分桶、统计条件概率。工程上更常用的做法是直接跑几个关键信噪点的 BER,看瀑布区位置是否符合预期。如果 EXIT 图开口很小,说明码率或映射方式需要调整,比如把码率从 3/4 降到 2/3,或者换更优的 LDPC 矩阵。
另一个实用技巧是固定译码器,只换映射方式对比。把格雷映射换成非格雷,其他不变,你会看到 BER 曲线在中等信噪比明显恶化,这就是映射方式对 LLR 质量的影响。反过来,固定映射,把 LDPC 码长从 648 加到 6480,瀑布区会左移,但译码耗时上升。这些对比做两三组,你对参数边界的直觉就建立起来了。
我自己在这类仿真里养成的习惯是:每改一个参数,先跑单信噪点、少次数,确认 BER 量级合理,再铺开跑曲线。直接上大规模仿真,一旦链路有错,浪费的是几小时甚至一整天的机时。希望帮到你。
本文还有配套的精品资源,点击获取