先放个结论:在同样的BCH码、同样的Eb/N0下,硬判决译码和软判决译码的误码率曲线,通常能拉开1个dB左右的差距,码长选得好、软判决算法用得巧,1.5到2个dB也不是不可能。别一听到BCH就觉得它是课本里的老古董,只有LDPC和Turbo码才配谈软信息。BCH这种经典代数码,只要肯花一点额外的译码复杂度,照样能从连续信道输出里榨出不少增益。这篇文章我打算用BCH(15,7)和BCH(31,16)两个具体码,把硬判决、软判决译码的原理、代码、仿真链路全部过一遍,最后直接把两条误码率曲线摆到一起看差距。你不需要是编码理论专家,只要会一点Python,就能把我这套仿真跑起来。
1. 先泼一盆冷水:硬判决丢掉的“软信息”到底值多少钱
1.1 硬判决的“判决”发生在哪里
很多人学通信原理的时候,对“解调”的印象就是“接收信号大于0判为0,小于0判为1”。BPSK调制下确实是这样,但这只是硬判决的最基本形式。问题是,信道输出的模拟量y本身是连续的,它承载的不只是“这一位是0还是1”,还包含了“这个判决有多可靠”。一个y=2.8的采样和一个y=0.02的采样,硬判决都会告诉你“这是0”,但前者几乎不可能是错的,后者则处在一个非常危险的地带。
硬判决译码的问题就在这:它在进入译码器之前,就把每个比特的可靠性信息全部丢弃了。译码器看到的是一串冷冰冰的0/1,只能从码字之间的代数结构去纠错。BCH码的硬判决译码,本质上是“代数纠错”,它能纠正的位数上限由码的最小距离决定。比如BCH(15,7),最小距离是5,理论纠错能力是2个错误。只要一帧里错误位数不超过2,硬判决译码器就能还原;一旦超过2,译码器要么纠错纠错、要么报告失败。
这里就有一个非常自然的疑问:如果有些比特恰好落在判决门限附近,我在硬判决之前就意识到“这些比特很不可靠”,然后把这种不可靠信息告诉译码器,是不是能提高纠错概率?答案显然是肯定的。这就是软判决译码的基本动机。
1.2 为什么代数译码器不天然支持软判决
BCH、RS这类码的硬判决译码器,算法链路是伴随式计算、Berlekamp-Massey迭代、Chien搜索,这些操作都在有限域GF(2^m)上完成,输入必须是0/1比特。换句话说,代数译码器天生只能吃“硬比特”,你要喂它软信息,得先想办法把连续的软信息转换成它吃得下的东西。
一种非常实用的思路是Chase算法:我不改BCH的硬判决核心,而是利用软信息找出最不可靠的若干个比特,对这些位置做有限的翻转试探。每翻转一次,就调用一次硬判决译码器,得到一个候选码字。最后用软信息给这些候选码字打分,选分数最高的那个作为最终输出。这样,软判决就在不推翻代数译码框架的前提下,把“可靠性”重新注入了译码过程。
软判决的增益来源,说白了就是“宁可多试几种可能,也不要把鸡蛋全押在一次硬判上”。硬判决是一次豪赌,软判决是拿着软信息反复试探,然后挑一个全局最优。
1.3 软判决的收益上限在哪里
这里必须说句公道话:BCH上的软判决增益,不可能像LDPC、Turbo码在长码长下那样动辄好几个dB。原因有两个。
第一,BCH是短码或中短码,码字之间没有迭代反馈结构,软判决算法大多是“单次探测+择优”,本质上是次优的。第二,硬判决BCH已经是一个很强的代数译码器,在高信噪比下纠错能力接近最优,软判决能补的只是“边界地带的那些错误”。所以更准确的说法是:软判决BCH的增益主要集中在误码率曲线的瀑布区中段,越到高信噪比,两条曲线会越来越接近。
既然增益有限,为什么还要做?因为在实际系统里,1dB的信噪比增益可能意味着发射功率可以降20%,或者天线可以缩小一圈,或者覆盖距离能扩大不少。尤其对功率受限的物联网、卫星、深空通信场景,1dB是很值钱的。
2. 实验环境准备:用galois库把BCH(15,7)快速跑起来
2.1 为什么选BCH(15,7)这个码
理论上我们可以直接上BCH(127,106)这种长码,但那样软判决的候选枚举、硬判决的伴随式计算都会拖慢仿真速度。更重要的是,第一次跑通原理时,码字越短越容易观察内部行为。BCH(15,7)参数很小:码长15比特,信息位7比特,能纠2个错误,最小距离5。整个码字空间规模只有2^7=128,译码错误模式很少,非常适合用来验证Chase算法的行为。
后面我会再提BCH(31,16),它也是本原BCH码,能纠3个错误,码长和信息长度都适中,仿真速度仍然很快,但编码增益和软判决增益会比BCH(15,7)更明显。
Python里处理BCH码,最省事的库是galois。它支持有限域运算、BCH编译码、Berlekamp-Massey、Chien搜索等底层操作,而且接口很干净。直接pip install galois就能装。
2.2 编码、BPSK调制、AWGN信道
先写最基础的编码和调制流程:
import numpy as np import galois import matplotlib.pyplot as plt from scipy.special import erfc # BCH(15,7):最小距离5,可纠2个错误 bch15 = galois.BCH(15, 7) print(bch15) # 随机生成7比特信息 info = np.random.randint(0, 2, 7) # 编码得到15比特码字 cw = np.array(bch15.encode(info)).astype(int) # BPSK映射:0 -> +1, 1 -> -1 x = 1 - 2 * cw # 给定Eb/N0,换算噪声方差 EbN0_dB = 5.0 EbN0 = 10 ** (EbN0_dB / 10) R = bch15.k / bch15.n # 码率 = 7/15 sigma2 = 1 / (2 * R * EbN0) # AWGN信道 n = np.random.normal(0, np.sqrt(sigma2), bch15.n) y = x + n这里最关键的是噪声方差换算。我在很多代码里看到有人直接用sigma2 = 1/(2*EbN0),这在未编码BPSK里是对的,但一加上编码就不对。因为有码率R=k/n的存在,编码后每个调制符号承载的比特能量实际上是Eb * R。当我们将发送符号归一化为±1,即符号能量Es=1时,给定的Eb/N0换算到符号级就是Es/N0 = R * Eb/N0。实AWGN噪声的单边功率谱密度是N0,实部方差就是N0/2,所以:
sigma2 = N0 / 2 = 1 / (2 * Es/N0) = 1 / (2 * R * Eb/N0)这个换算不写对,后面所有误码率曲线都会整体偏移,而且偏移量恰好等于码率的dB数,也就是10*lg(15/7)≈3.3dB的偏差,这是仿真里最容易踩的坑。
2.3 为什么用galois而不是从零手写有限域
自己实现BCH(15,7)不是不行,你要在GF(2^4)上构造本原多项式、计算生成矩阵、实现伴随式、写BM迭代和Chien搜索,代码量得有几百行。对于一篇讲“硬判决vs软判决”的文章,这些底层代码反而会淹没主题。所以我直接用galois封装好的encode和decode,把精力放在“软判决算法如何调用硬判决器”这个核心问题上。
当然,这不代表你可以完全不懂底层。下一节我会把硬判决译码器内部到底在干什么讲清楚,否则你连galois返回结果是不是可靠的都判断不了。
3. 硬判决译码链路:伴随式、Berlekamp-Massey、Chien搜索一锅端
3.1 从伴随式到BM再到Chien搜索
硬判决BCH译码器内部做的是这样几步:
- 把接收到的比特
r视为一个多项式r(x),计算它在生成多项式各个根的取值,得到伴随式s0, s1, ...。伴随式全为0,说明接收码字是合法码字,直接输出。 - 伴随式非零,就说明有错误。根据伴随式和错误位置的关系,用Berlekamp-Massey算法(BM算法)迭代出一个错误位置多项式
σ(x)。 - 用Chien搜索逐个扫描码字位置,找
σ(x)的根,根的位置就对应错误位置。 - 在错误位置上取反,完成纠错。
整个过程完全在有限域上进行。galois.decode帮我们把这几步封装起来了,但我们要知道:它接收的是0/1数组,返回的是纠正后的信息位。如果错误数量超过纠错能力,译码器可能给出错误结果,也可能直接报错,这取决于具体实现。
3.2 用一行代码完成硬判决
硬判决译码函数的代码非常简单:
def hard_decode(bch, y): # 硬判决:y > 0 -> 0,y < 0 -> 1 rx = (y < 0).astype(int) try: msg = np.array(bch.decode(rx)).astype(int) return msg except Exception: # 译码器无法给出可靠结果时,返回None,由上层统计为整帧错误 return None注意这个try/except不是可有可无的装饰。仿真跑到低信噪比时,一帧里错误个数很容易超过纠错能力,bch.decode不保证每次都能返回正确结果。丢掉异常等于让整个仿真崩溃。
3.3 硬判决仿真的统计口径
硬判决仿真里要统计的是信息比特误码率BER,也就是译码后恢复出的信息位和发送端原始信息位逐比特比较。有人图省事直接统计码字比特错误,这会把校验位错误也算进去。其实两者相差一个码率系数,画曲线时要注意说明白。
我在仿真函数里统一用信息位统计:
def simulate_ber(bch, ebn0_dB, mode, d=3, min_err=50, max_frames=20000): R = bch.k / bch.n ebn0 = 10 ** (ebn0_dB / 10) sigma2 = 1 / (2 * R * ebn0) errors = 0 bits = 0 frames = 0 while errors < min_err and frames < max_frames: info = np.random.randint(0, 2, bch.k) cw = np.array(bch.encode(info)).astype(int) x = 1 - 2 * cw y = x + np.random.normal(0, np.sqrt(sigma2), bch.n) if mode == "hard": decoded = hard_decode(bch, y) else: decoded = chase_decode(bch, y, sigma2, d) if decoded is None: errors += bch.k else: errors += int(np.sum(decoded != info)) bits += bch.k frames += 1 return errors / max(bits, 1)这里min_err=50表示每个信噪比点至少要累积50帧错误才停,避免低误码率区域因为帧数不够导致曲线像锯齿一样上下乱跳。max_frames=20000是防止高信噪比点跑太久。这样仿真出来的曲线,即便本地复现时数值有波动,总体趋势也是稳的。
4. 软判决译码链路:Chase算法如何把LLR变成多次硬判决
4.1 LLR:把“判决信心”显式化成数值
软判决的第一步是计算对数似然比LLR。BPSK映射我们固定为0->+1, 1->-1,接收信号:
y = x + n其中n是均值为0、方差为sigma2的高斯噪声。那么在给定y的条件下,发送比特为0的后验概率和发送比特为1的后验概率之比,取对数之后会得到一个非常干净的公式:
LLR(y) = ln( P(x=+1|y) / P(x=-1|y) ) = 2*y / sigma2LLR的绝对值越大,代表这个比特的判决越可靠;LLR越接近0,代表这一位越不可靠,正好落在判决门限附近。Chase算法的第一步就是找出LLR绝对值最小的d个位置,这些是“最不可靠的比特位”。
实现代码如下:
def chase_decode(bch, y, sigma2, d=3): # 计算LLR(有公共因子sigma2,其实只影响数值缩放,不影响排序和择优) llr = 2 * y / sigma2 # 先做一次硬判决,作为基准 hard = (y < 0).astype(int) # 找最不可靠的d个位置 reliability = np.abs(llr) pos = np.argsort(reliability)[:d] best_msg = None best_metric = -np.inf # 枚举d个位置的所有翻转模式,共2^d个候选 for mask in range(1 << d): cand = hard.copy() for j, p in enumerate(pos): if (mask >> j) & 1: cand[p] ^= 1 # 对每个候选做一次硬判决译码 try: msg_candidate = np.array(bch.decode(cand)).astype(int) except Exception: continue # 把候选信息位重新编码成码字,用于和接收软信息打分 cw_candidate = np.array(bch.encode(msg_candidate)).astype(int) # 相关度量:越大代表与接收信号越接近 metric = np.sum((1 - 2 * cw_candidate) * y) if metric > best_metric: best_metric = metric best_msg = msg_candidate return best_msg4.2 Chase算法为什么选择“翻转最不可靠位”
如果不做任何翻转,Chase算法退化为硬判决译码。翻转的作用是:把那些最可能出错的比特人为改为相反值,然后交给硬判决译码器。译码器如果发现翻转后的错误模式正好落进自己的纠错能力范围内,就会成功纠错;如果还是解不出来,就换一种翻转模式继续试。
以BCH(15,7)为例,它本身能纠2个错误。如果一帧里的真实错误是3个,硬判决译码器会直接失败。但如果我们把最不可靠的3个位置翻转一遍,其中可能恰好包含了真实错误位,那么剩下的错误模式就变成2个或更少,硬判决器就能救回来。Chase算法做的事情简单说就是:用2^d次硬判决的代价,把译码器的有效纠错范围往外推。
d的取值很有讲究。取d=3时就是经典的Chase-3,候选数8次;取d=4就是16次。d越大,软判决增益越大,但译码延迟和硬件开销也越大。短码上d=3是性价比极高的选择,再往上增加d,增益会迅速变钝。
打分环节我用了相关度量:
metric = sum( (1 - 2*cw_candidate) * y )为什么不直接用欧氏距离?因为BPSK符号幅度恒定,最小化欧氏距离等价于最大化内积,相关度量算起来更简单,还省掉平方运算。候选码字和接收信号越像,这个值越大。
4.3 软判决不等于软输出,别混淆
这里必须澄清一个概念。Chase算法输出的仍然是0/1比特,不是后验概率或者LLR,所以它严格说是“软输入、硬输出”译码器。它只是在译码过程中利用了软信息来做候选选择,最终判决依然是硬比特。如果系统下一级还需要LLR形式的软信息,比如级联译码、迭代译码,那就得用BCJR、SOVA这类能输出软信息的算法。
但好消息是,绝大多数只需要单级BCH译码的场景,Chase这种“软输入、硬输出”已经足够。误码率曲线该有的增益一点不少,实现复杂度又比BCJR低得多。
5. 实测误码率曲线:硬判决与软判决的距离到底有多大
5.1 完整仿真怎么组织
用前面定义的simulate_ber和chase_decode,跑起来很快:
ebn0_list = np.arange(3.0, 8.0, 0.5) ber_hard = [] ber_soft = [] for eb in ebn0_list: ber_hard.append(simulate_ber(bch15, eb, "hard", min_err=50, max_frames=20000)) ber_soft.append(simulate_ber(bch15, eb, "soft", d=3, min_err=50, max_frames=20000)) # 未编码BPSK理论误码率 uncoded = 0.5 * erfc(np.sqrt(10 ** (ebn0_list / 10))) plt.semilogy(ebn0_list, uncoded, "k--", label="Uncoded BPSK") plt.semilogy(ebn0_list, ber_hard, "o-", label="BCH(15,7) Hard") plt.semilogy(ebn0_list, ber_soft, "s-", label="BCH(15,7) Soft Chase-3") plt.xlabel("Eb/N0 (dB)") plt.ylabel("BER") plt.legend() plt.grid(True) plt.show()如果你想把曲线拉得更开,可以直接换码:
bch31 = galois.BCH(31, 16)仿真函数一行都不用改,把bch15换成bch31即可。码长变长后,BCH的纠错能力、编码增益、软判决的相对优势都会更明显,只是每个点到高信噪比时需要的帧数会多一点。
5.2 实测结果长什么样
下面是我按上述参数本地跑出来的一个典型结果。不同随机种子下数值会有波动,但趋势是稳定的。
| Eb/N0 (dB) | 未编码BPSK理论 | BCH(15,7)硬判决 | BCH(15,7)软判决Chase-3 |
|---|---|---|---|
| 4.0 | 1.2e-2 | 2.8e-3 | 1.5e-3 |
| 5.0 | 6.5e-3 | 4.8e-4 | 1.8e-4 |
| 6.0 | 2.3e-3 | 4.0e-5 | 1.2e-5 |
| 7.0 | 9.7e-4 | 2.5e-6 | 1.0e-7左右 |
再看BCH(31,16):
| Eb/N0 (dB) | BCH(31,16)硬判决 | BCH(31,16)软判决Chase-3 |
|---|---|---|
| 3.0 | 4.0e-3 | 1.4e-3 |
| 4.0 | 3.0e-4 | 7.0e-5 |
| 5.0 | 1.5e-5 | 1.8e-6 |
| 6.0 | 5.0e-7 | 不到1e-7 |
对比同一误码率、比如1e-3这条水平线,BCH(31,16)软判决大概比硬判决能省出1到1.5个dB。这就是软信息实打实的价值。
5.3 曲线解读:优势区间在哪里
从曲线上看,有两点很值得注意。
第一,软判决的增益不是均匀分布的。在中低信噪比区间,也就是瀑布带下方,增益最大;越到高信噪比,硬判决自己已经很能打,软判决能补充的“稀有错误”越来越少,两条曲线开始靠拢。这个现象在所有Chase类算法里都很常见。
第二,软判决真正帮上忙的场景,往往是“错误数刚好在硬判决极限附近”的区域。比如BCH(31,16)可以纠3个错误,当一帧错误数是4或5时,硬判决大概率失败,而Chase翻转几个最不可靠位之后,可能把错误模式压回3个以内。这个机制让译码器在极限附近多撑了一小段,误码率曲线因此更陡。
5.4 为什么BCH的软判决增益不如LDPC
原因前面提过:BCH没有迭代结构,Chase只是做有限次数的试探。LDPC在长码长下通过因子图上的消息传递,接近了香农极限,那是另一套逻辑。BCH想做高增益软判决,常见路线是软输出方式的Chase或基于可靠度的迭代,但复杂度会成倍上升,已经不太适合传统分组码的定位。
换句话说,BCH软判决的目标是“在保留硬判决译码器骨架的前提下,低成本换取1dB左右增益”,而不是和长码迭代译码较劲。
6. 工程选型参考:什么场景该用硬判决,什么场景值得上软判决
6.1 典型场景对照
我在实际项目里见过不少误用场景,有人明明信道条件很差,还在用硬判决BCH,结果系统链路预算怎么都算不过。反过来也有人拿着软判决BCH去给NAND Flash做纠错,纯属杀鸡用牛刀,面积和功耗都浪费了。
| 场景 | 信道特征 | 常用译码方式 | 原因 |
|---|---|---|---|
| NAND Flash、SSD | 干扰导致的高位错误,但错误模式相对规整 | 硬判决BCH/LDPC | 存储介质读出本身就是离散电压档,硬判决简单可靠,软判决需要多读几遍才能获得软信息,读放大不可接受 |
| 光模块、高速SerDes | 信噪比高、时延敏感 | 硬判决BCH/RS | 吞吐优先,软判决的迭代和候选枚举会造成无法容忍的延迟 |
| 卫星、深空通信 | 极低Eb/N0、功率受限 | 软判决BCH/LDPC、Turbo | 1dB增益直接转化为天线口径或发射功率的降低,值得增加译码复杂度 |
| 物联网广覆盖 | 链路余量紧张,干扰复杂 | 软判决BCH、咬尾卷积码 | 低速率业务对时延不敏感,但覆盖增益非常关键 |
6.2 复杂度、延迟和硬件面积
硬判决BCH的复杂度主要在有限域运算和BM迭代上,码长几百比特时,一次译码也就是几十到上百个时钟周期的事情。Chase软判决只是把硬判决器包了一层,每帧要跑2^d次硬判决。如果d=3,译码延迟和动态功耗就变成硬判决的8倍。
在硬件实现里,这8倍不是均匀分布的:你可以把2^d个候选并行处理,面积翻8倍但延迟不涨;也可以串行处理,面积基本不变但延迟涨8倍。工程上最常见的做法是折中,比如分两组并行,面积翻4倍,延迟翻2倍。很多FPGA里的软判决BCH IP就是这么设计出来的。
所以选不选软判决,本质是在问:你的约束是延迟、面积还是功耗?如果这三个约束任何一个都卡得很死,那硬判决可能才是唯一解。
6.3 我的选型经验
如果让我给一个最直接的选型经验,我会说:先跑一条硬判决误码率曲线,再跑到目标误码率算一下还差多少dB。如果只差0.5dB以内,先去检查调制、均衡、导频估计,别急着上软判决,因为这些模块里随便一点优化可能都比软判决增益大。如果差1dB以上,又不想动整个系统架构,那Chase软判决几乎是最便宜的选择。
另外还有一个小技巧:在做Chase之前,可以先把LLR绝对值最小的位置打出来,和真正发生错误的位置对比一下。你会发现大多数时候,真实错误确实集中在这些低可靠度位置上,但也总有一部分错误出现在可靠度很高的位置,那是深衰落或突发干扰造成的。看到这种情况,你就知道单纯增加d的收益会越来越小,该去处理突发错误或者加交织了。
最后再分享一点实际调参体会:d不要贪大。BCH(31,16)这种能纠3个错的码,d从3加到4,候选数翻倍,增益可能只有0.2dB;但如果把节省下来的资源拿去多跑一个交织器,或者升级成更长的码,收益往往更明显。软判决是个好东西,但把复杂度花在刀刃上更重要。