ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

咬尾卷积码(13,17)编码与Viterbi解码实战指南

咬尾卷积码(13,17)编码与Viterbi解码实战指南 简介面向通信与信息论学习者的卷积码仿真资料包聚焦1317卷积码与咬尾卷积码的编码、解码及性能分析适合需要动手验证信道编码原理、设计软输入软输出系统的学生或工程师。压缩包共11个文件以10个MATLAB脚本和1个Excel状态转移表为主脚本细致覆盖编码器实现、对数似然比计算、对数最大后验概率解码及主流程控制表格则用于查看状态转移关系便于对照理解。已有1092人学习下载内容实用性强。通过运行主程序可完整观察从编码、调制、信道模拟到解码的整个过程并借助解码脚本理解软输出维特比算法与迭代译码细节同时不同编码率的对比实现有助于分析编码增益变化为调整参数、优化通信系统提供可复用的实验基础。1. 咬尾卷积码到底是什么以(13,17)为入门码型做短帧突发通信的人都会对卷积码的“尾巴”又爱又恨。(13,17)卷积码是一个经典的高约束码型纠错能力强但每帧末尾必须补3个归零比特帧长只有几十比特时这个开销能把有效码率从1/2拉到0.45甚至更低。咬尾卷积码把初始状态设置成和终了状态相同让编码在环上闭合省掉尾巴而不牺牲距离特性。本文以(13,17)为例从生成多项式、移位寄存器讲起一步一步做咬尾编码、Viterbi解码并列出调试时最容易翻车的几个现场。适合正在做控制信道、短包传输或协议物理层仿真的同学读完应该能自己把编码器和解码器跑通。2. 读(13,17)生成多项式从八进制数到移位寄存器与网格图2.1 八进制13和17在移位寄存器上的物理含义耳熟能详的卷积码生成多项式一般用八进制写比如(7,5)是约束长度3(13,17)是约束长度4码率1/2。把13展开成4位二进制是101117展开是1111。这里约定最高位对应D^3、最低位对应D^0因此13对应g0(D)D^3D117对应g1(D)D^3D^2D1。有些资料从右往左读字位结果会变成另一个码字所以对接别人的代码之前先确认顺逆序。有了这两个多项式编码器结构就很清楚了3级移位寄存器输入比特逐拍进入输出两路校验比特。抽头位置由多项式里“1”的系数决定g0抽当前输入、延时2位和延时3位g1把当前输入和三个寄存器全部抽一遍。写成Python就是这样一个函数def conv_encode(seq, init_state0): # (13,17)卷积码K4R1/2 # 状态bit0是最新输入bit1和bit2是更早的输入 r0 (init_state 0) 1 r1 (init_state 1) 1 r2 (init_state 2) 1 out [] for b in seq: # g0 D^3 D 1: 当前输入、延时2、延时3 c0 b ^ r1 ^ r2 # g1 D^3 D^2 D 1: 当前输入和三个寄存器全抽 c1 b ^ r0 ^ r1 ^ r2 out.append(c0) out.append(c1) # 移位寄存器旧r0变r1旧r1变r2当前输入进r0 r2, r1, r0 r1, r0, b final_state (r2 2) | (r1 1) | r0 return out, final_state这段代码里的状态定义必须记牢bit0是最新进入寄存器的输入bit1是上一拍的输入bit2是再上一拍的输入。c0对应13c1对应17输出顺序是c0、c1交替。调用时init_state取0到7返回的第一个值是长度为2 * len(seq)的码字列表第二个值是编码结束后寄存器的状态。这个终态在下一章就是咬尾的关键。2.2 网格图、状态转移和最小自由距离卷积码的本质是一个有限状态机3个寄存器一共8个状态每个状态在输入0或1时迁移到下一状态同时输出2比特。把时间轴展开就是网格图。从全零状态出发最受关注的是“离开又回到全零状态”的那条路径它的汉明重量决定了纠错能力。对(13,17)这个多项式组合最小自由距离d_free是6比(7,5)的5要好代价是状态数从4翻倍到8Viterbi解码的每符号计算量也跟着翻倍。可以手推一小段来验证编码器有没有写错。输入序列取[1,0,1,1]初始状态0第1拍输入1三个寄存器都是0c01c11输出11移位后状态变成1第2拍输入0此时r01、r10、r20c00c11输出01状态变成2第3拍输入1寄存器是r00、r11、r20c00c10输出00状态变成5第4拍输入1寄存器r01、r10、r21c00c11输出01状态变成3。把输出串起来是11010001终态是3。用上面conv_encode跑一遍结果一样。这个手算动作看起来笨但排错时比看网格图快得多尤其是确认寄存器和多项式抽头位序到底对不对时。2.3 尾巴开销短帧场景不是小数目普通卷积码编码结束后要把移位寄存器归零通常再送K-13个0也就是额外产生6个比特。帧长N100时这6个冗余比特占掉3/103约2.9%的码率帧长N30时就变成9.1%N20时开销超过13%。对控制信道、信令块、小包业务来说这是实打实的吞吐损失。更隐蔽的问题是归零操作让解码器默认终态为0初态也为0解码器不需要关心起点。如果去掉尾巴解码器不知道起点是8个状态里的哪一个这就是咬尾卷积码必须处理的难点。先别急下一章先把编码端做对把初态和终态闭合起来解码端再回来对付这个“未知起点”。3. 咬尾编码流程用(13,17)在短帧上省掉3个尾巴比特3.1 咬尾的数学条件初始状态等于末状态普通卷积码编码器从全零状态启动编码一个N比特的帧后要再输入3个0把寄存器清回零。咬尾卷积码完全不送尾巴但要求编码结束时寄存器状态和开始时一模一样也就是S_N S_0。因为编码N个比特后寄存器里装的只会是输入序列的最后3个比特和启动状态已经无关所以咬尾条件的实质是初态必须等于输入序列的最后K-1位。对(13,17)的3级寄存器来说初态由序列最后3个比特唯一确定。换句话说咬尾编码并不会固定某个初态每一帧的初态都由这一帧的数据自己决定。这个特性让编码器不再是“零启动”那么简单也决定了解码器不能当作普通Viterbi直接用。这里有个边界要注意输入序列长度N必须大于等于K-1也就是至少3比特。实际短帧通信里N通常不少于16不会踩到但写通用函数时最好加一个断言。3.2 预编码法两遍编码得到咬尾输出最常见也最不容易错的做法是预编码法先以全零状态把整帧编码一遍拿到终态然后把终态设成初态重新再编码一遍。第一遍的输出直接丢掉第二遍的输出才是真正要发送的咬尾码。这样做的好处是逻辑通用换其他生成多项式时不需要手推初态公式。def tb_conv_encode(seq): # 第一遍从全零状态编码得到终态 _, final_state conv_encode(seq, init_state0) # 第二遍用终态作为初态重新编码输出即为咬尾码 out, final_state_again conv_encode(seq, init_statefinal_state) # 咬尾条件二次编码后的终态必须回到初态 assert final_state_again final_state, 咬尾状态闭合失败 return out, final_state逻辑说明第一遍编码结束时寄存器内容与启动状态无关已经变成输入序列的最后3比特。第二遍用这个终态当初态编码完整个序列后寄存器内容还是那最后3比特于是初态等于末态。断言的作用是防止你把状态位序写错一旦闭合失败立刻报警而不是到解码端才知道错。参数说明final_state是一个0到7的整数表示3级寄存器的状态out长度是2 * len(seq)没有额外尾巴。如果帧长是48比特普通卷积码输出102比特咬尾码只有96比特节省的6比特就是尾巴开销。3.3 一次性编码与码率对比既然初态就是输入序列最后3比特也可以跳过第一遍编码直接用最后3比特设定初态def tb_conv_encode_once(seq): # 状态bit0是序列最后1位bit1是倒数第2位bit2是倒数第3位 init seq[-1] | (seq[-2] 1) | (seq[-3] 2) out, _ conv_encode(seq, init_stateinit) return out, init这种方式节省一遍编码时间但需要你记住状态位序稍微改错一个移位方向就满盘皆输。我一般有两套代码快速验证时用tb_conv_encode_once上硬件逻辑或给别人复用时用两遍的tb_conv_encode让代码里的assert去兜底。两种方式结果完全相同输出都是2N比特。普通卷积码和咬尾卷积码的对比可以整理成一张表对比项普通卷积码咬尾卷积码输入长度N的输出比特2(N3)2N额外归零比特60有效码率N/[2(N3)]1/2解码初态已知为0未知可能为0到7解码终态已知为0等于初态但初态未知这张表其实说明了一个重要事实咬尾编码省的6个比特是真实收益帧越短收益越明显。收尾的难点变成解码端如何估计起点这正是下一章要处理的。4. 咬尾Viterbi解码环绕、双向与软判决的参数怎么选4.1 初态未知把网格扯成了一个环普通Viterbi解码从已知初态0开始末尾又靠归零回到0所以网格是一个有起点有终点的“梳子”。咬尾码的初态和终态相等但未知所有8个状态都可能是起点也同时是终点网格变成了一个圆柱面。直接照搬普通Viterbi头尾几十个比特的可靠性会明显下降尤其是帧长只有几十比特时损失可能是好几dB。处理环上搜索的常见方案有三类环绕Viterbi算法、双向Viterbi算法以及把咬尾码当卷积码配合迭代的近似算法。工程上最常用的是前两类。它们的目标都是找到一条绕环一周且累计度量最小的路径区别只是搜索方向和收敛策略。4.2 环绕Viterbi算法WAVA三步走WAVA的思路是把接收序列当成周期信号连续解码多个周期让路径充分收敛再从中间的某处回溯。具体分三步初始化8个状态的路径度量全部置0避免起点偏好用普通Viterbi的加比选对接收序列循环处理帧长N就处理N步一共迭代2到3个周期在最后一个周期的中间段选一个状态作为回溯起点回溯设定深度后取中间一段作为解码输出。参数上(13,17)是K4的状态数8码型回溯深度通常取20到30比特就够但WAVA场景建议放到40或更大。迭代周期和帧长有关N48时迭代3个周期足够N200时可以只迭代2个周期。核心循环如下def wava_decode(rx, trans, iterations3, traceback40): S 8 # 所有状态初始度量都为0不偏袒任何起点 path_metric [0] * S survivor [] step_count len(rx) // 2 for _ in range(iterations): for i in range(step_count): new_metric [float(inf)] * S new_surv [-1] * S for s in range(S): for b in (0, 1): ns, c0, c1 trans[(s, b)] branch branch_metric(rx[2*i], rx[2*i1], c0, c1) cand path_metric[s] branch if cand new_metric[ns]: new_metric[ns] cand new_surv[ns] s path_metric new_metric survivor.append(new_surv) # 回溯从最后一次迭代的中间附近开始回溯traceback步 # 取最中间的一段输出绕开环头尾的收敛盲区 start_step len(survivor) - traceback // 2 state min(range(S), keylambda s: path_metric[s]) decoded [] for i in range(start_step, start_step - traceback, -1): state, b survivor[i][state] decoded.append(b) decoded.reverse() return decoded这段代码的trans是预计算好的状态转移表branch_metric是分支度量函数。注意回溯方向是从顶事件前向回溯取中间段而不是末尾段因为环上每一周期的起点和终点都会有收敛盲区中间段最可靠。实际调试时可以先跑零噪声冒烟测试确认解码输出和原始序列完全一致再调回溯深度。4.3 双向Viterbi用前后向度量夹住唯一路径WAVA是工程上最省内存的方案但迭代过程中可能收敛到局部次优。双向Viterbi则从两个方向同时搜索前向计算每个状态在当前时刻的最小累积度量后向也计算一份最后把两个度量相加在环上选择总和最小的状态路径。这样能更贴近最大似然解代价是内存翻倍需要同时保存前后向度量和回溯指针。对(13,17)这种8状态码型双向Viterbi的前后向各是8个度量单帧48比特时总状态空间不算大软件仿真完全跑得动。硬件上如果不想加倍存储还是用WAVA更实际。选择依据很简单追求性能和可解释性就双向追求资源可控就WAVA。4.4 软判决、量化和度量归一化的实际参数咬尾Viterbi的性能很大程度取决于软判决量化。高斯信道下3位软判决比硬判决有约2dB增益4位软判决再增加0.2到0.3dB再往上收益很小。常见的8级量化映射是{-7, -5, -3, -1, 1, 3, 5, 7}对应BPSK解调后的置信度。分支度量用欧氏距离或曼哈顿距离都可以曼哈顿距离实现简单性能损失可以忽略。路径度量必须做归一化尤其定点实现时。做法是每处理一个符号找到当前8个状态的最小度量把所有状态减去这个最小值防止累积值无限增长导致溢出。浮点仿真里不归一化也能跑但遇到长迭代周期时一样会出问题。一张参数表可以当作起点参数推荐值软判决量化3~4 bit分支度量曼哈顿距离或欧氏距离回溯深度40帧长很短时至少20WAVA迭代周期2~3个帧周期度量归一化每步减去当前最小度量这些参数不是拍脑袋定的。回溯深度和自由距离有关(13,17)的d_free是6路径要足够长才能把竞争路径彻底分开迭代周期受帧长影响帧越短越需要多绕几圈让度量收敛。5. 咬尾(13,17)避坑5个调试现场与修法5.1 误码率比普通卷积码还差初态被默认成0了现象用标准Viterbi解码咬尾编码的帧输出误码率比普通卷积码更高尤其帧头几十比特几乎全错。原因咬尾码的初态是数据决定的可能从8个状态里任何一个出发普通Viterbi从状态0起跑相当于给每帧强加了一个错误起点。解决改用WAVA或双向Viterbi不要保留“初态为0”的假设。5.2 编码输出长度不对多发6个比特现象咬尾编码后输出长度是2(N3)而不是预期的2N。原因没有去掉归零尾巴用的是普通卷积码编码器编码完又补了3个0。解决把末尾那3个输入删掉用tb_conv_encode或tb_conv_encode_once检查输出长度是否为2N。如果输出多了说明还在走普通卷积路径。5.3 预编码求终态时用错了状态位顺序现象第一遍编码得到终态设成初态后再编码断言失败解码端无论怎么调都错。原因状态bit0到底是最近输入还是最早输入不同实现定义相反你用猜的就会错。解决固定定义bit0为最近输入bit1为上一拍bit2为再上一拍并在代码里保留assert final_state_again final_state。位序一旦搞反咬尾闭合永远不成立。5.4 WAVA回溯点选在了还没收敛的区域现象短帧N40到60时零噪声正常加噪声后偶发整帧错误误码率曲线出现不该有的平台。原因回溯深度不够或者从第一周期末尾回溯而那里还没绕到收敛区。解决迭代3个周期从最后一个周期的中间位置回溯回溯深度至少40。可以用这个经验口诀取中间、别取头尾宁长勿短。5.5 定点实现时路径度量溢出现象跑一会儿之后解码输出变成全零或乱跳浮点仿真却完全正常。原因路径度量没有归一化累加值超出定点数范围通常uint16溢出。解决每处理一个符号找到当前8个状态的最小度量全部减去这个最小值。这一段代码要放在加比选之后、下一符号开始之前。min_metric min(path_metric) path_metric [m - min_metric for m in path_metric]把这段放进循环里定点溢出问题基本能消失。调试时还可以定时打印path_metric的最大值确认它被限制在一个稳定范围内。6. 最后一步用一个小验证工程测出咬尾(13,17)的BER想验证整套实现我会先跑一个零噪声冒烟测试随机生成一个N48的0/1序列用tb_conv_encode编码再用已经实现的WAVA解码器把码字解回来要求完全一致。这一步不通过后面的AWGN曲线没有意义。通过之后再叠加高斯噪声用硬判决或3位软判决测BER横轴是Eb/N0。一个值得养的测试习惯是固定几个已知序列比如全零、全一、单脉冲把它们当作回归用例。全零是咬尾编码最容易出问题的序列因为初态和终态都是0看起来像普通卷积码但万一实现的解码器只对全零有效全一序列会立刻暴露。单脉冲序列可以检查最小自由距离附近的错误模式对(13,17)来说d_free路径的汉明重量是6脉冲位置不同错误表现也不同。我习惯先把N固定成48因为48是很多短帧协议的实际长度也是WAVA迭代3个周期最自然的验证点。跑出BER曲线后再改到96和128确认回溯深度不用跟着变性能曲线没出现反常拐点。如果出现了平台第一反应不是去调信道模型而是回到第5章的列表检查初态、回溯点和归一化。咬尾(13,17)的代码量不大但每一步都有顺序依赖编码器先验证终态闭合解码器先验证零噪声再加噪声测统计性能。我自己吃过一次亏直接跳到AWGN仿真结果性能差2dB排查半天发现是状态位序反了。从那以后冒烟测试始终排在第一位。希望帮到你。本文还有配套的精品资源点击获取
返回列表