☰
(15,11)汉明码BSC与AWGN信道误码率仿真及编码增益分析
2026/9/28 12:50:44 网站建设 项目流程

简介:本资源面向通信原理、信道编码课程的学习者与仿真实践者,围绕(15,11)汉明码在BSC与AWGN两类信道下的误码率性能展开,提供可直接运行的MATLAB完整代码。包内共12个文件,以6个m脚本为核心,另含2张jpg结果图、2份docx说明文档、1个txt关键词文件及1个asv备份文件,压缩包约219KB,结构紧凑、便于快速查阅与复现。仿真分两条链路:一是发送数据经15/11/4汉明编码后通过BSC信道,再经汉明译码并统计BER;二是编码后接BSSK调制、AWGN信道、BPSK解调与汉明译码,最终计算误码率。运行脚本即可直接出图,直观对比两种信道下编码前后的性能差异。已有580人学习下载,适合需要完成课程设计、实验报告或验证纠错编码增益的读者参考,也可作为进一步扩展调制方式与信道参数的仿真基础。

1. 从(15,11)汉明码说起:为什么一个比特的纠错能力值得反复算

(15,11)汉明码是经典分组码里最常被拿来练手的一种:码长 15,信息位 11,校验位 4,最小汉明距离 3,能纠正 1 位错、检测 2 位错。很多做通信链路仿真、FPGA 编码模块验证、甚至做存储 ECC 对比的人,第一步都会拿它开刀。原因很直接——结构简单、编解码都能手算、误码率曲线又足够典型,能同时把 BSC 和 AWGN 这两类信道的差异讲清楚。实际工程里,网络接口误码率这种指标背后往往就是编码增益在撑着,而(15,11)汉明码正好是理解编码增益从哪来的最小样本。这篇就把编码矩阵、译码查表、BSC 与 AWGN 两条仿真链路、参数怎么设、曲线怎么读,一步步拆开讲,目标是让你照着能跑出可复现的 BER 曲线,而不是停在公式层面。

2. (15,11)汉明码的编码与译码:从生成矩阵到伴随式查表

2.1 为什么选(15,11)而不是(7,4)

(7,4)汉明码码长 7、信息位 4、校验位 3,同样纠 1 位错,但码率只有 4/7≈0.571;而(15,11)码率是 11/15≈0.733。码率越高,同样带宽下有效信息越多,这是选它的第一个理由。第二个理由是(15,11)的校验位是 4 位,伴随式有 16 种状态,其中 1 种对应无错,剩下 15 种刚好一一对应 15 个可能的单比特错误位置,这种“满映射”让译码查表非常干净,没有浪费状态。第三个理由是它和扩展汉明码、缩短汉明码的衔接自然,工程里做码率适配时经常从它派生。

代价也要说清楚:最小距离仍然是 3,只能纠 1 位错,遇到 2 位错会误纠成另一个合法码字。所以在信噪比很低、错误成串的信道里,它的表现会明显不如 BCH 或 LDPC。选它是因为场景匹配——错误独立、单比特为主、对时延和实现复杂度敏感。

2.2 生成矩阵与校验矩阵的构造

(15,11)汉明码通常写成系统码形式,生成矩阵 G 是 11×15,左边是 11×11 单位阵,右边是 11×4 校验部分。校验矩阵 H 是 4×15,列向量是 1 到 15 的二进制表示。下面这段 Python 直接构造 G 和 H,并验证 G 的行空间与 H 的零空间一致。

import numpy as np def build_hamming_15_11(): # H 的列是 1..15 的 4 位二进制表示,按列排 cols = [] for i in range(1, 16): col = [(i >> b) & 1 for b in range(3, -1, -1)] # 高位在前 cols.append(col) H = np.array(cols, dtype=int).T # 4x15 # 把 H 化成系统形式 [P | I4],通过列交换找到单位阵位置 # 这里直接按标准构造:校验位放在最后 4 列 # 重新排列列,使后 4 列构成 I4 # 选取列 1,2,4,8 对应二进制 0001,0010,0100,1000 perm = [0,1,3,7,2,4,5,6,8,9,10,11,12,13,14] # 0-based 索引 H_perm = H[:, perm] # 此时 H_perm 的后 4 列应为 I4 assert np.array_equal(H_perm[:, -4:], np.eye(4, dtype=int)), "后4列不是单位阵" # 系统形式 H_sys = [P | I4],P 是 4x11 P = H_perm[:, :11] # 生成矩阵 G_sys = [I11 | P^T] G = np.hstack([np.eye(11, dtype=int), P.T]) # 11x15 return G, H_perm G, H = build_hamming_15_11() print("G shape:", G.shape) print("H shape:", H.shape) # 验证 G 的行空间是 H 的零空间:G @ H^T = 0 print("G @ H^T =") print((G @ H.T) % 2)

这段代码的关键点有三个。第一,H 的列向量必须覆盖所有非零 4 位向量,这样伴随式才能唯一指示错误位置。第二,列置换是为了把 H 变成系统形式,方便直接读出生成矩阵,置换本身不影响纠错能力,只影响码字比特的排列顺序。第三,(G @ H.T) % 2必须全零,这是编码和校验一致性的硬性检查,如果这里不为零,后面所有仿真都不可信。

参数上,range(1,16)对应 15 个列,range(3,-1,-1)控制二进制位序,位序反了会导致 H 的列和错误位置映射错位,译码时就会纠到错误的比特上。这个坑在第一次实现时非常常见。

2.3 伴随式译码与查表实现

译码的核心是算伴随式 s = r · H^T,然后查表得到错误图样。因为(15,11)只有 15 种单比特错误,直接建一个长度 16 的字典即可。

def syndrome_decode(r, H): # r: 长度 15 的 0/1 数组 s = (H @ r) % 2 # 4 位伴随式 s_tuple = tuple(s.tolist()) # 构建伴随式到错误位置的映射 syndrome_to_pos = {} for pos in range(15): e = np.zeros(15, dtype=int) e[pos] = 1 syn = tuple(((H @ e) % 2).tolist()) syndrome_to_pos[syn] = pos # 全零伴随式表示无错 if s_tuple == (0,0,0,0): return r.copy(), 0 if s_tuple in syndrome_to_pos: pos = syndrome_to_pos[s_tuple] r_hat = r.copy() r_hat[pos] ^= 1 return r_hat, 1 # 出现无法纠正的错误图样(2位及以上),这里直接返回原值 return r.copy(), -1 # 测试:随机信息位,编码,加1位错,译码 np.random.seed(0) msg = np.random.randint(0, 2, 11) codeword = (msg @ G) % 2 err = np.zeros(15, dtype=int) err[5] = 1 r = (codeword + err) % 2 decoded, flag = syndrome_decode(r, H) print("译码是否纠正:", flag) print("译码后信息位:", decoded[:11]) print("原始信息位: ", msg)

逻辑说明:先算伴随式,如果全零直接返回;否则查表找到错误位置并翻转。flag返回 1 表示纠正了 1 位错,返回 -1 表示伴随式不在单错表里,说明发生了 2 位及以上错误,此时译码器无法可靠纠正。参数上,H @ r得到的是 4 维向量,转成 tuple 才能做字典键;pos的范围是 0 到 14,对应码字的 15 个位置。

注意:如果信道中 2 位错误恰好产生了一个合法码字的伴随式(概率存在),译码器会误纠成第三个码字,这就是最小距离 3 的固有局限,不是实现 bug。

3. BSC 信道下的误码率仿真:翻转概率怎么设、曲线怎么跑

3.1 BSC 模型与理论误码率

BSC(二进制对称信道)是最简单的离散信道模型:每个比特以概率 p 独立翻转,正确概率 1-p。对于(15,11)汉明码,译码后仍然出错的概率可以写成解析式。码字错误概率 P_cw 是发生 2 位及以上错误的概率,因为 1 位错能被纠正:

P_cw = 1 - (1-p)^15 - 15p(1-p)^14

而信息位误码率 P_b 近似为 P_cw × (平均错误信息位数 / 11)。在 p 很小时,主要贡献是 2 位错,可以进一步近似。但工程上更常用蒙特卡洛仿真来验证,因为解析式在 p 较大时误差明显。

下面这段代码跑 BSC 仿真,p 从 1e-4 到 0.1 对数扫描,每个点至少 1e5 个码字。

def bsc_simulate(p, num_codewords=200000): errors = 0 total_bits = 0 for _ in range(num_codewords): msg = np.random.randint(0, 2, 11) cw = (msg @ G) % 2 # BSC 翻转 flip = (np.random.rand(15) < p).astype(int) r = (cw + flip) % 2 dec, flag = syndrome_decode(r, H) # 统计信息位错误 errors += np.sum(dec[:11] != msg) total_bits += 11 return errors / total_bits p_list = [1e-4, 3e-4, 1e-3, 3e-3, 1e-2, 3e-2, 0.1] ber_bsc = [] for p in p_list: ber = bsc_simulate(p, num_codewords=100000) ber_bsc.append(ber) print(f"p={p:.1e}, BER={ber:.6e}")

参数说明:num_codewords决定统计精度,p 越小需要的码字数越多,因为错误事件稀疏。p=1e-4 时,100000 个码字里大约只有 150 个比特翻转,落到信息位上的错误更少,所以 BER 估计的方差较大。实际跑的时候建议 p<1e-3 时把码字数提到 1e6,或者用重要性采样。flip = (np.random.rand(15) < p)是独立翻转的标准写法,不要用np.random.binomial逐比特调用,那样慢很多。

3.2 编码增益怎么读

把 BSC 的 BER 曲线和未编码的 BER=p 画在一起,能看到明显的下弯。在 p=1e-2 时,未编码 BER 是 1e-2,编码后大约降到 1e-3 量级,差不多一个数量级的改善。这个改善来自 1 位错被纠正,而 2 位错概率是 C(15,2)p^2(1-p)^13 ≈ 105 p^2,在 p=1e-2 时约 1e-2,和 1 位错概率 15p≈0.15 相比小一个量级,所以大部分错误被吃掉了。

但要注意,BSC 下 p 超过 0.05 以后,2 位错和 3 位错占比上升,编码增益迅速衰减,甚至可能因为误纠导致比不编码更差。这就是为什么实际系统里汉明码通常用在误码率本来就不高的场景,比如内存 ECC、短距有线链路。

提示:BSC 仿真里不要用同一个随机种子跑所有 p 点,否则不同 p 之间的错误事件相关,曲线会不光滑。每个 p 点单独设种子或者不设种子。

4. AWGN 信道下的误码率仿真:从软判决到硬判决的差距

4.1 AWGN 模型与 BPSK 映射

AWGN 信道下,发送的是 BPSK 符号:0 映射为 +1,1 映射为 -1,接收信号 y = x + n,n 是均值 0、方差 σ² 的高斯噪声。信噪比通常用 Eb/N0 表示,对于 BPSK,Eb/N0 = 1/(2σ²)。仿真时先设定 Eb/N0 的 dB 值,反推 σ,再生成噪声。

(15,11)汉明码的硬判决译码是:先对每个接收符号做判决得到 0/1,再用伴随式译码。软判决则直接利用接收信号的幅度信息,但汉明码的软判决译码不像卷积码那样有标准 Viterbi,通常用 Chase 算法或者直接做最大似然。工程上更常见的是硬判决,因为实现简单,而且汉明码的软硬差距不大(约 1-2 dB)。

下面代码实现 AWGN 下的硬判决仿真。

def awgn_simulate(ebn0_db, num_codewords=100000): ebn0 = 10 ** (ebn0_db / 10) # BPSK: Eb/N0 = 1/(2*sigma^2) => sigma = sqrt(1/(2*ebn0)) sigma = np.sqrt(1 / (2 * ebn0)) errors = 0 total_bits = 0 for _ in range(num_codewords): msg = np.random.randint(0, 2, 11) cw = (msg @ G) % 2 # BPSK 映射:0->+1, 1->-1 symbols = 1 - 2 * cw noise = sigma * np.random.randn(15) rx = symbols + noise # 硬判决 hard = (rx < 0).astype(int) dec, flag = syndrome_decode(hard, H) errors += np.sum(dec[:11] != msg) total_bits += 11 return errors / total_bits ebn0_list = [0, 1, 2, 3, 4, 5, 6, 7, 8] ber_awgn = [] for ebn0 in ebn0_list: ber = awgn_simulate(ebn0, num_codewords=50000) ber_awgn.append(ber) print(f"Eb/N0={ebn0} dB, BER={ber:.6e}")

参数说明:sigma = sqrt(1/(2*ebn0))是 BPSK 在 AWGN 下的标准关系,注意这里的 Eb 是信息比特能量,不是编码比特能量。如果按编码比特能量算,需要乘码率 11/15。这个细节在对比未编码曲线时特别容易搞错,导致编码增益看起来偏大或偏小。rx < 0是硬判决,0 对应 +1,所以负值判为 1。

4.2 硬判决与未编码 BPSK 的对比

未编码 BPSK 的 BER 是 Q(sqrt(2*Eb/N0)),其中 Q 是高斯尾概率。把这条理论曲线和(15,11)硬判决仿真曲线画在一起,能看到在 BER=1e-5 时编码增益大约 2 dB 左右。这个增益比 BSC 下看起来小,因为 AWGN 下错误本身就是随机的,汉明码只能纠 1 位,对 2 位错无能为力,而 AWGN 的噪声是连续的,硬判决后的错误分布和 BSC 类似但相关。

如果想看软判决的潜力,可以把每个符号的似然信息传给译码器,做 Chase 译码:对最不可靠的几位做翻转尝试,选伴随式为零的候选。实现上比硬判决复杂,但能再拿 0.5-1 dB。实际工程里要不要上软判决,取决于你的译码器有没有余量做多次尝试,以及时延预算。

注意:AWGN 仿真里num_codewords在低 Eb/N0 时可以少一些,高 Eb/N0 时错误稀疏,需要更多码字才能统计到足够的错误。一般保证每个点至少 100 个错误事件,否则 BER 估计不可信。

5. 避坑与排查:仿真跑不通时先看这五条

5.1 现象:BER 曲线在高信噪比下不下降反而翘起

原因:伴随式查表时把 2 位错误判成 1 位错,译码器翻转了错误的比特,导致错误数增加。这在硬判决里很常见,因为 2 位错产生的伴随式可能和某个 1 位错的伴随式相同(当两个错误位置的和等于第三个位置时)。解决:在译码函数里加一个标志,当伴随式对应的错误图样权重超过 1 时,不纠正,直接输出原值。或者用最小距离译码,选汉明距离最近的码字。

5.2 现象:BSC 仿真在 p 很小时 BER 恒为零

原因:码字数不够,p=1e-4 时 100000 个码字里期望的 2 位错事件只有约 105×1e-8×100000≈0.1 个,根本统计不到。解决:p<1e-3 时把码字数提到 1e6 以上,或者用解析式补点。另一个办法是重要性采样,人为增加错误概率再加权,但实现复杂,一般不建议新手用。

5.3 现象:AWGN 下编码增益和理论对不上

原因:Eb/N0 的定义搞混了。未编码 BPSK 的 Eb 是信息比特能量,编码后每个信息比特对应 15/11 个编码比特,如果仿真时按编码比特能量算噪声,等效 Eb/N0 会差 10log10(11/15)≈-1.35 dB。解决:统一按信息比特能量定义,噪声方差用 sigma=sqrt(1/(2*ebn0)),不要额外乘码率。

5.4 现象:译码后信息位顺序错乱

原因:生成矩阵构造时列置换没有同步到译码端,或者系统码形式下信息位不在前 11 位。解决:在编码和译码之间固定一个列排列,最好直接用系统码形式,信息位固定在前 11 位,校验位在后 4 位。如果用了列置换,把置换表存下来,译码后按逆置换恢复。

5.5 现象:仿真速度太慢,跑一晚上出不了图

原因:Python 循环逐码字处理,没有向量化。解决:把码字生成、噪声添加、硬判决全部向量化,一次处理一批码字。比如生成 10000×15 的矩阵,用矩阵乘法算伴随式,用 numpy 的广播做查表。速度能提升几十倍。下面是一个向量化的伴随式译码片段。

def syndrome_decode_batch(R, H): # R: (N, 15) 0/1 矩阵 S = (R @ H.T) % 2 # (N, 4) # 构建伴随式到位置的映射 syn_to_pos = {} for pos in range(15): e = np.zeros(15, dtype=int) e[pos] = 1 syn_to_pos[tuple(((H @ e) % 2).tolist())] = pos # 转成整数索引加速 S_int = S[:,0]*8 + S[:,1]*4 + S[:,2]*2 + S[:,3] corrected = R.copy() for syn_val, pos in syn_to_pos.items(): idx = syn_val[0]*8 + syn_val[1]*4 + syn_val[2]*2 + syn_val[3] mask = (S_int == idx) corrected[mask, pos] ^= 1 return corrected

这个批量版本把逐码字循环变成了矩阵运算,10 万个码字几秒就能跑完。参数上,S_int把 4 位伴随式转成 0-15 的整数,方便做向量化比较;mask选出所有伴随式相同的码字,统一翻转对应位置。

6. 把 BER 曲线用起来:从仿真到链路预算的一个具体技巧

跑出 BSC 和 AWGN 两条曲线只是第一步,真正有用的是把曲线转成链路预算里的编码增益数字。我一般会做一件事:在目标 BER 处(比如 1e-5)读出未编码和编码后的 Eb/N0 差值,这个差值就是可用的编码增益。对于(15,11)汉明码,AWGN 硬判决下大约是 2 dB,BSC 下在 p=1e-3 附近大约等效 1.5-2 dB。这个数字可以直接拿去和 LDPC、BCH 的增益对比,判断值不值得换编码方案。

具体操作:把仿真得到的 BER 数组和 Eb/N0 数组做插值,找到 BER=1e-5 对应的 Eb/N0。如果仿真点不够密,用对数域线性插值。下面这段代码演示怎么从仿真数据里提取编码增益。

from scipy.interpolate import interp1d # 假设 ber_awgn 和 ebn0_list 已经跑出来 # 未编码 BPSK 理论 BER def q_func(x): return 0.5 * math.erfc(x / math.sqrt(2)) ebn0_fine = np.linspace(0, 10, 1000) ber_uncoded = [q_func(np.sqrt(2 * 10**(e/10))) for e in ebn0_fine] # 编码曲线插值(注意 BER 可能为 0,需要处理) ber_awgn_arr = np.array(ber_awgn) valid = ber_awgn_arr > 0 if valid.sum() > 1: f_enc = interp1d(np.log10(ber_awgn_arr[valid]), np.array(ebn0_list)[valid], fill_value='extrapolate') ebn0_enc_at_1e5 = f_enc(np.log10(1e-5)) # 未编码在 1e-5 处的 Eb/N0 f_unc = interp1d(np.log10(ber_uncoded), ebn0_fine, fill_value='extrapolate') ebn0_unc_at_1e5 = f_unc(np.log10(1e-5)) gain = ebn0_unc_at_1e5 - ebn0_enc_at_1e5 print(f"编码增益 @ BER=1e-5: {gain:.2f} dB")

这个技巧的关键是插值要在对数域做,因为 BER 曲线在半对数坐标下近似直线,线性域插值误差大。另外,仿真点如果 BER 掉到 0,要先过滤掉,否则 log10 会出问题。我自己的习惯是每个 Eb/N0 点至少跑 100 个错误事件,不够就加大码字数,这样插值出来的增益才靠谱。

最后说一个血泪教训:早期我做仿真时图省事,BSC 和 AWGN 共用了一套译码函数,结果 BSC 下没问题,AWGN 下硬判决的比特翻转概率和 BSC 的 p 并不等价,导致两条曲线对比时结论完全错了。后来我固定做法——BSC 直接用翻转概率 p,AWGN 先算硬判决后的等效 p,再分别验证,这样两条链路的物理意义才清晰。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询