模拟喷泉码这个词第一次摆在面前的人,往往会先愣一下——喷泉码不是数字域里那套LT、Raptor的东西吗,怎么还有“模拟”的?而“自适应模拟喷泉码在无线信道中的应用”这个题目,把三个信息量很大的词堆在了一起:模拟喷泉码、自适应、无线信道。说白了,它讨论的是一类不走传统“先信道编码再调制”串行路线的无速率传输方案:编码符号不再是0/1比特,而是一个可以直接叠加到载波上的实数值,收端收到多少个符号就译多少,收得越多译得越准,中间不需要精确的握手和码率协商。这类方案特别适合链路质量抖动剧烈、包长又短的场景,比如低功耗广域物联网、短报文卫星链路、水声通信、车联网的侧链路广播。如果你正在做基带链路仿真、想找一个比固定码率LDPC更“随遇而安”的编码结构,或者你只是好奇“实数编码符号”到底怎么译码、怎么评估,那这篇内容应该能给你一些直接能抄的东西。下面我按自己搭仿真时的思路,从设计动机、结构拆解、实操步骤到踩过的坑,完整走一遍。
1. 为什么无线链路上要重新捡起喷泉码这套思路
1.1 数字喷泉码在无线链路里的三个尴尬
数字喷泉码的理论很漂亮:发射端源源不断吐编码包,接收端收够了就停,理论上不需要反馈,天然适配组播和信道条件未知的场景。但真把它丢进无线链路,问题马上就来了。第一个尴尬是“停止时刻”的判断。LT码和Raptor码要译码成功,需要一个可判定的完成条件,工程上几乎都靠外挂CRC或者反馈ACK。这个反馈在无线链路上是有延迟的,链路越差、往返时间越长,你多发的那部分冗余就越多,频谱效率的损失直接体现在吞吐上。
第二个尴尬是码率的颗粒度。喷泉码号称无速率,但实际译码需要“整个符号”到位,收端要么译出来、要么没译出来,中间没有软信息可以利用。在低信噪比区,数字喷泉码的译码器拿到的是硬判决或者不太可靠的软信息,性能曲线会在某个点上突然“断崖”,而不是平滑地劣化。第三个尴尬是分离定理的失效。有限码长下,把信道编码和调制分开设计、分成两级优化的做法本来就有性能损失;而在时变信道里,这个损失会被放大,因为编码器根本不知道当前这一帧会经历什么样的衰落。
我自己在做短包仿真时对这个感受很深:包长从几千比特压到几百比特,固定码率的LDPC和Turbo的编码增益掉得厉害,而喷泉类方案又受限于反馈开销和硬判决损失,两边都不讨好。模拟喷泉码恰恰是从这个夹缝里长出来的方案——它把“编码符号”从离散字母表搬到实数域,让软信息从编码阶段就一路带着走,同时保留了无速率的骨架。
提示:不要把模拟喷泉码理解成“模拟信号的喷泉码”。它仍然是离散时间、离散信息的编码,只是编码输出取连续实数值,信道是AWGN,噪声是加性的。这个区别在建模时一定要拎清楚,否则仿真里信噪比的定义会写错。
1.2 模拟喷泉码的核心转变:让编码符号变成实数
模拟喷泉码的基本构造其实非常朴素。假设有N个信息符号,取值是BPSK的±1(也可以推广到高阶星座),第m个编码符号这样生成:先按某个度分布随机挑出d个信息符号,再给每个被选中的符号配一个权重,最后把所有加权项加起来。写成式子就是 y_m 等于权重乘以对应信息符号的累加和,接收端看到的是这个和再加上高斯噪声。因为权重是实数、信息符号也是实数,编码输出自然就是实数,可以直接送进调制器,不需要再做星座映射这一步。
这个结构带来两个直接好处。第一,译码可以全程在实数域做置信传播,消息是高斯分布的均值和方差,而不是离散的LLR表,软信息没有被量化损失掉。第二,码率是连续可调的。你多发一个符号,接收端就多一条线性方程,信息量就多一点点,不存在“多发一个包浪费一整个包”的问题。用生活化的比喻:数字喷泉码像是往杯子里倒水,一次倒一杯,倒多了溢出来;模拟喷泉码像是用滴管滴水,滴多少是多少,杯子满了就停。
当然代价也是有的。实数域BP的复杂度比二进制BP高,每个校验节点的消息更新都要算方差;权重集的选择会直接影响性能上界,权重选大了噪声被放大,选小了方程之间的区分度不够;另外随机图构造必然带来短环,短环会让BP的消息在几个节点之间空转,收敛变慢甚至不收敛。这些问题在后面几节会逐个拆。
2. 自适应模拟喷泉码的整体设计拆解
2.1 生成矩阵:一次编码在物理层到底做了什么
把上面那个构造用矩阵写出来,就是 y = Gx + n,其中x是N维信息向量,y是M维接收向量,G是M×N的稀疏矩阵,每一行只有d个非零元,位置随机、取值来自权重集W。这个形式和压缩感知里的观测矩阵非常像,但两者的问题设定正好相反:压缩感知是M远小于N、要求x稀疏;模拟喷泉码是M略大于N、x是稠密的,稀疏性在图结构上而不在信号上。这个类比很有用,因为压缩感知里成熟的BP、AMP、OMP一类算法,基本都能平移过来做译码,只是要按过定系统调整迭代停止条件和先验。
从物理层角度看,G的每一行代表“一次线性叠加”,在模拟域实现就是一组加权求和。如果权重取整数,比如1、2、3,那这一行在硬件上就是几个支路的增益相加,用开关电容或者电阻网络都能做,代价极低。这也是这类方案在实际系统里比较有吸引力的原因之一——它的编码器不是一个复杂的状态机,而是一个可配置的加权求和网络,甚至可以把权重固定下来,只随机化连接关系。相比LDPC那种需要大规模并行走线的结构,模拟喷泉码的编码端实现难度低不少。
不过这里有个容易忽略的细节:G的随机化程度决定了性能的下界。如果连接关系是伪随机的且种子固定,那么同一个码字在不同帧之间会重复出现短环,某些信息符号可能长期被少数几个校验节点覆盖,导致译码时的置信度分布严重不均。我的做法是每帧重新抽取连接关系,把种子和帧号绑定;如果帧长固定、要求确定的图结构,那就要在设计阶段做一次离线搜索,把包含4环的行挑出来重抽。
2.2 权重集与度分布:两个必须一起调的旋钮
权重集W是模拟喷泉码独有的设计自由度。数字喷泉码没有这个概念,每个编码包就是若干个信息包的异或,等价于权重恒为1。一旦权重可以取不同的值,问题就变成了“怎么选一组数,让线性方程组既好解、又不放大噪声”。这里有个直观的权衡:权重越大,不同行之间的“区分度”越高,G的条件数越好,但校验节点更新时噪声方差会按照权重的平方被放大,等效信噪比下降。极端情况下如果权重取到10,那这一行的观测就基本是噪声了。
实践中比较常见的做法是取小整数集合,比如{1, 2, 3}或者{1, 2, 4},让权重的动态范围控制在两三倍以内。另一个思路是取正负对称的集合,比如{±1, ±2},这样每一行的和值分布更接近零均值,对BP的初始消息更友好。我在仿真里对比过这两类,正负对称的集合在低信噪比区收敛更快,但在高信噪比区优势不明显,反而因为负权重的存在让部分硬件实现变复杂。如果你的场景是纯软件仿真或者数字域的等效实现,倾向选对称集;如果奔着低功耗硬件去,老老实实用正整数集。
度分布和权重集必须联合优化,不能分开调。度分布决定了每个编码符号连接多少个信息符号,平均度太低会导致译码的“涟漪”起不来,BP在第一步就卡死;平均度太高会让每一行的计算量暴涨,而且外信息的方差累积过快,置信度的动态范围被压缩。经验上,平均度落在8到14之间比较稳妥,短码(N在几百量级)可以往低了取,长码可以往高了取,因为长码的环更分散。
2.3 自适应到底适应的是什么
这个词是全篇最容易被误解的。自适应在模拟喷泉码的语境下通常有两个层次,要分清。第一个层次是符号级自适应:发射端知道当前信道的瞬时质量(比如通过导频估计出的信噪比),据此调整每个编码符号的发射功率或者有效权重。信道好的时候加大权重、提高单符号的信息承载量;信道差的时候把权重压小,保证观测的可靠性。这个层次的自适应,本质上是把功率控制和编码设计揉在了一起。
第二个层次是码级自适应,也就是根据信道状态选择不同的度分布和权重集。因为模拟喷泉码的等效码率是连续可调的,理论上你在任意信噪比下都能找到一组参数,让吞吐逼近容量。工程上的做法是离线跑一组信噪比点,每个点用差分进化或者密度进化优化出一套度分布,做成本地表,运行时根据估计的信噪比查表切换。这个思路和LDPC的codebook切换是一样的,只是模拟喷泉码切换的是分布参数而不是整个校验矩阵,表小得多。
还有第三种更“野”的理解,就是完全不做显式自适应,靠无速率特性自然适配。收端不问发端要多少,收到多少算多少,译不出来就继续收。这种做法在单向广播或者反馈极贵的场景里非常实用,代价是收端要承受一定的过收冗余,通常比理想自适应多花20%到40%的符号。我个人倾向于把这个叫“被动自适应”,它和前面两个层次并不冲突,可以叠加使用。
3. 从零搭一套仿真:编码端、信道、译码端
3.1 参数怎么定:先算清楚再动手
动手写代码之前,先把几个量算清楚,否则跑出来的曲线你根本不知道问题出在哪。信息符号数N先定下来,短包场景取256或者512,中等包长取1024,验证长码性能取4096以上。过收比例(overhead)定义为(M-N)/N,这个值在模拟喷泉码里通常比数字喷泉码低,因为实数观测携带的信息量更大,1e-3误码率下大概20%到30%就够,具体取决于度分布和信噪比。
然后是复杂度核算。编码端每个符号需要d_avg次乘加,M个符号就是M乘d_avg次运算。取N=1024、d_avg=10、overhead=25%,则M=1280,编码端约1.3万次乘加每帧。假设符号率是1兆符号每秒,每帧1280个符号占1.28毫秒,那编码端算力需求约每秒1000万次乘加,一颗中低端MCU都能扛住。译码端是迭代的,复杂度是迭代次数乘以M乘以d_avg再乘以一个常数,取迭代50次就是每秒5亿次量级,这时候就得考虑用向量化或者定点实现了。这个账一定要提前算,不然仿真跑到一半发现一帧要跑十秒,参数就没法扫了。
| 参数 | 短包场景 | 中等包长 | 长码验证 | 说明 |
|---|---|---|---|---|
| 信息符号数 N | 256 | 1024 | 4096 | BPSK取值±1 |
| 平均度 d_avg | 8 | 10 | 12 | 与N成正相关 |
| 过收比例 | 30% | 25% | 20% | 低信噪比区适当放宽 |
| BP最大迭代 | 60 | 80 | 100 | 配合早停使用 |
| 目标误码率 | 1e-3 | 1e-4 | 1e-5 | 决定统计帧数 |
信噪比的定义要特别小心。因为编码输出是实数加权和,它的平均功率不等于1,所以Eb/N0不能直接套用BPSK的公式。正确的做法是先算编码符号的方差,对每一行求权重平方和再对度分布取期望,得到平均符号能量E_s,再用E_s/N0换算。如果这一步算错了,整条曲线会平移好几dB,白跑。
3.2 编码端实现与稀疏结构存储
编码端的核心是把图结构建好。不要用稠密矩阵,N到4096的时候M×N就是五百多万个浮点数,内存和乘法都吃不消。用行列表的形式存储,每行记一个索引数组和一个权重数组,这是最省事也最快的。
import numpy as np def sample_degree(omega, rng): ds = np.array(list(omega.keys())) ps = np.array(list(omega.values()), dtype=float) ps = ps / ps.sum() return int(rng.choice(ds, p=ps)) def build_graph(N, M, omega, W, seed=0): rng = np.random.default_rng(seed) rows = [] for _ in range(M): d = sample_degree(omega, rng) idx = rng.choice(N, size=d, replace=False) w = rng.choice(W, size=d, replace=True) rows.append((idx, w.astype(float))) return rows def encode(u, rows): y = np.empty(len(rows)) for m, (idx, w) in enumerate(rows): y[m] = w @ u[idx] return y这段代码有两个可以立刻改进的点。第一,rng.choice在N很大时是O(N)的,跑几万帧会明显拖慢,可以换成先用permutation取前d个,或者用一个洗牌池维护。第二,同一行里允许重复权重是必要的,否则度高的行权重组合数不够,但要注意别让同一行出现同一个索引两次,那等于把某个符号的权重翻倍,会引入隐式的确定性。我一般还会在构完图之后做一次4环检测,把有4环的行重抽,短码下这一招能省下0.3到0.5dB。
信道部分就是标准的AWGN:y_rx = y + np.sqrt(sigma2) * rng.standard_normal(M)。sigma2按前面算的平均符号能量来定。如果要加衰落,就在每个符号上乘一个瑞利或者莱斯系数,注意这时候接收端要拿到相干检测后的等效模型,否则BP的消息更新公式要重写。
3.3 实数域BP译码的实现细节
译码是整套方案里最需要小心的地方。二进制BP里的消息是LLR,是一维的;实数域BP里的消息是高斯分布,要同时传均值和方差,所以每条边上要存两个数。校验节点的更新逻辑是:对第m行,已知y_m约等于该行所有变量贡献之和加噪声,那么要对第k条边给出关于x_k的估计,就用y_m减去其他所有边的贡献,再除以权重w_k,方差相应传播。
def bp_decode(y, rows, N, sigma2, max_iter=80, tol=1e-6): M = len(rows) # 每条边上的消息:mean 和 var m2v_mean = [[0.0] * len(r[0]) for r in rows] m2v_var = [[1e6] * len(r[0]) for r in rows] v2c_mean = [[0.0] * len(r[0]) for r in rows] v2c_var = [[1e6] * len(r[0]) for r in rows] # 变量节点 -> 校验节点的边索引表 var_edges = [[] for _ in range(N)] for m, (idx, _) in enumerate(rows): for k, j in enumerate(idx): var_edges[j].append((m, k)) xhat = np.zeros(N) for it in range(max_iter): # 第一步:变量节点合并外信息(精度加权平均) post_mean = np.zeros(N) post_var = np.zeros(N) for j in range(N): num, den = 0.0, 0.0 for (m, k) in var_edges[j]: p = 1.0 / (m2v_var[m][k] + 1e-12) num += m2v_mean[m][k] * p den += p if den == 0: post_mean[j], post_var[j] = 0.0, 1e6 continue post_mean[j] = num / den post_var[j] = 1.0 / den for (m, k) in var_edges[j]: p = 1.0 / (m2v_var[m][k] + 1e-12) d_ = den - p if d_ <= 1e-12: v2c_mean[m][k], v2c_var[m][k] = 0.0, 1e6 else: v2c_mean[m][k] = (num - m2v_mean[m][k] * p) / d_ v2c_var[m][k] = 1.0 / d_ # 第二步:校验节点消去其他边的贡献 for m, (idx, w) in enumerate(rows): cm = [w[k] * v2c_mean[m][k] for k in range(len(idx))] cv = [(w[k] ** 2) * v2c_var[m][k] for k in range(len(idx))] sm, sv = sum(cm), sum(cv) for k in range(len(idx)): m2v_mean[m][k] = (y[m] - (sm - cm[k])) / (w[k] + 1e-12) m2v_var[m][k] = (sigma2 + (sv - cv[k])) / (w[k] ** 2 + 1e-12) xhat = np.where(post_mean >= 0, 1.0, -1.0) # 早停:重编码残差小于门限 resid = 0.0 for m, (idx, w) in enumerate(rows): resid += (y[m] - w @ xhat[idx]) ** 2 if resid < 3.0 * len(rows) * sigma2: break return xhat, it, resid这段代码里有两个点值得单独说。第一个是早停条件。模拟喷泉码没有CRC,怎么知道译对了?用重编码残差:把硬判决结果代回编码方程算一遍,残差应该接近噪声功率乘以M。我用的门限是3倍的期望残差,实测在1e-3误块率附近比较稳,门限设太小会误判(明明译对了但残差偏大),设太大则会放过错误帧。这个门限最好按你的目标误块率扫一遍定下来,不要照搬。
第二个是方差的初值。流程图里通常写“初始化为无穷大”,实际代码里必须给一个有限的大数,因为后面要做倒数。这个初值不能太小,否则第一轮变量节点的置信度就是错的;也不能太大,太大反而会在倒数的时候数值下溢。我的经验是按照信道方差乘以十的数量级来给,比较稳。
3.4 结果曲线怎么读、怎么判断方案的好坏
跑完仿真至少要看三条曲线,只看BER是不够的。第一条是BER随Eb/N0的变化,用来判断方案的编码增益,同时要叠一条理想的二进制LDPC或者香农限做参照,不然你不知道自己离理论还有多远。第二条是吞吐随信噪比的曲线,吞吐要按“成功译出的信息比特数除以实际消耗的符号时间”来算,过收冗余要计进去,这才是工程上有意义的指标。第三条是平均迭代次数随信噪比的变化,这条曲线能告诉你自适应切换点应该放在哪,也能暴露BP的病态问题。
读曲线的时候有个经验:模拟喷泉码的BER曲线在低信噪比区往往比较陡,然后在高信噪比区出现一个“拖尾平台”。拖尾一般是两类原因造成的,一是度分布里高度的行太多,噪声被权重平方放大,形成误差底;二是图里有顽固的短环,某些变量节点长期收不到有效外信息。前者靠调权重集和截断度分布解决,后者靠重新构图或者加交织。这个平台的判断很重要,如果你的目标误码率是1e-5,而平台在1e-4,那整个方案就是不可用的,必须回头改设计。
4. 调参与排错实录:那些跑不出曲线的坑
4.1 译码不收敛的四类原因
第一类图结构问题。最简单的表现是迭代到最大次数,重编码残差还在噪声功率的十倍以上,BER停在0.1左右不动。这种大概率是图里有变量节点完全没被任何一行覆盖,或者覆盖次数只有一次,BP给它传的外信息始终是先验,等于这个符号永远解不出来。排查办法是统计每个变量节点的连接度,最小度必须是2以上,出现0或者1就立刻重构图。这个检查我一般写成断言,构图后立刻跑一遍。
第二类是方差传播发散。表现是迭代到二三十轮之后,某些边的方差变成NaN或者负值。根源是校验节点更新里的除法,当权重很小或者减去其他贡献后余量接近零的时候会爆掉。前面代码里加的1e-12只是在掩耳盗铃,真正稳妥的做法是在校验节点更新前做一次下限截断,把方差限制在sigma2的百分之一以上,把均值的绝对值限制在一个合理区间。这个截断不会改变收敛点,只是防止数值炸掉。
第三类是信噪比失配。接收端估的sigma2和实际噪声方差差了3dB以上,LLR的尺度就全错了。好消息是实数域BP对尺度误差有一定容忍度,只要不是差一个数量级,通常只影响收敛速度不影响最终BER;坏消息是如果差得太多,置信度的相对权重大乱,会直接把某些正确的符号推翻。调试时把真实的sigma2喂进去对比一下,如果曲线立刻好了5dB,那就说明你的噪声估计模块有问题。
第四类其实是思路问题不是bug:度分布和信噪比不匹配。低信噪比下用了高平均度的分布,每一行的噪声被放大得太厉害;高信噪比下用了低平均度的分布,方程数量不够,过收比例再高也补不回来。这个不是改代码能解决的,得重新优化分布。我一般会在三四个典型信噪比点各跑一次密度进化,做成本地表,运行时根据估计信噪比选最近的表项。
4.2 权重集和度分布的调参经验
权重集的经验前面提了一部分,这里补充几条实操的。第一,权重集里不要出现互为整数倍的元素,比如{1,2,4}看起来没问题,但2和4的组合等价于把两个1和一个2做替换,会让不同行之间出现隐式的线性相关,BP在迭代后期会在这几行之间振荡。我吃过这个亏,把4换成3之后拖尾平台直接掉了半个数量级。第二,权重的个数不用太多,三到四个足够,多了意义不大还增加硬件配置复杂度。第三,如果要做不等错误保护,可以把重要的信息符号分配到高权重的边上,这是一个很自然的扩展,不需要额外的编码结构。
度分布方面,别迷信教科书上的鲁棒孤波分布。那是为理想的数字喷泉码设计的,搬到模拟域性能会差一截。原因是鲁棒孤波分布的形状是为了保证涟漪不中断,而模拟域的约束条件多了权重的噪声放大和实数观测的信息量,最优点会明显右移。我自己的做法是用一个参数化的分布族,比如以两个峰值位置和一个衰减指数为参数,然后用差分进化在目标信噪比下优化误块率,迭代五十代左右就能收敛到比教科书分布好1dB以上的结果。这个过程很耗算力,但只做一次,做完存表。
注意:优化度分布时目标函数要选误块率或者吞吐,不要选误码率。误码率对分布不敏感,容易优化到一个看起来很好但实际用不了的点。另外优化时的帧数要够,至少统计到两百个错误帧,否则优化结果会被噪声带着跑。
4.3 常见问题速查表
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
| BER卡在0.1不动 | 存在度为0或1的变量节点 | 构图后加最小度断言,重抽 |
| 迭代后期出现NaN | 校验节点更新除零或方差溢出 | 方差下限截断、均值范围截断 |
| 高信噪比出现拖尾平台 | 权重集有整数倍关系,或度高行过多 | 换权重集、截断度分布上界 |
| 曲线整体左移好几dB | 平均符号能量算错,Eb/N0定义不对 | 重算E_s,按权重平方和期望换算 |
| 低信噪比完全无法启动 | 平均度太低,初始涟漪不足 | 提高d_avg到10以上 |
| 单帧运行时间过长 | 稠密矩阵存储、无早停 | 改行列表存储,加重编码残差早停 |
| 收敛慢但最终BER正常 | 短环多,方差更新保守 | 4环检测重抽,适度提高迭代上限 |
| 帧间性能波动大 | 随机图种子未随帧变化 | 把种子和帧号绑定 |
5. 落地场景与后续可以扩展的方向
5.1 这套方案真正适合的三类场景
第一类是反馈昂贵或者干脆没有反馈的单向链路。广播、卫星下行、水声这类链路的共同点是往返时间远超帧长,等反馈回来信道早就变了。模拟喷泉码的被动自适应特性正好补上这个缺口,收端自己决定什么时候停,发端只管按固定速率吐符号。
第二类是短包、低时延场景。传统方案在短包下编码增益损失严重,而模拟喷泉码的结构简单、没有长交织、没有复杂的打孔,译码的启动延迟就是一个符号的时间。工业无线控制、车联网的周期性状态广播都符合这个画像。
第三类是信道质量动态范围特别大的链路。因为码率连续可调,它在信噪比大范围波动时不需要频繁切换编码方案,一个码字应付全程,避免了切换带来的同步和信令开销。这一点在低轨卫星这类多普勒和仰角变化剧烈的场景里很有价值。
5.2 工程上还能往哪扩
往硬件方向走,最直接的想法是把权重固定成小整数,用开关电容阵列实现加权求和,编码器可以做成纯无源的。这个思路在超低功耗的传感节点上很有吸引力,因为编码阶段几乎不耗动态功耗。往算法方向走,可以把BP换成近似消息传递,在观测矩阵接近高斯独立同分布时收敛更快,代价是对图结构的随机性要求更高。
另一个值得试的方向是把模拟喷泉码和稀疏码多址结合起来做联合设计。多址场景下多个用户同时发,接收端看到的叠加信号天然就是多个编码符号的和,这个结构跟模拟喷泉码的线性叠加非常契合,有希望把多用户检测和信道译码做成一个整体。这个想法我自己只跑过初步的仿真,多用户之间的功率不平衡处理起来比单用户复杂得多,还没到能给出结论的程度。
我在实际搭这套仿真的过程中最深的一个体会是:模拟喷泉码的性能上限,八成取决于图结构和度分布这两件事,译码算法本身的改进空间反而有限。很多人一开始就想去折腾更高级的译码器,其实把4环清干净、把平均度调到合适的区间,收益比换算法大得多。另外一点,别急着上长码,先用N等于256把整条链路跑通,把残差早停的门限定下来,再去扩到1024和4096,能省掉大量返工。