☰
Vivado卷积编码与维特比译码IP核链路配置与仿真排错
2026/11/3 7:00:26 网站建设 项目流程

FPGA 上做前向纠错,翻车的点几乎从来不在卷积编码算法本身,而在 Vivado 里那两个 IP 核的配置对不上号:编码端填的生成多项式顺序颠了、译码端回溯深度设得太浅、AXI4-Stream 的 tdata 位宽差一位、尾比特没送干净,最后波形上看数据“在动”,比特却有一半是错的。这篇东西就把我在 Vivado 里用卷积编码器 IP 核加维特比译码 IP 核跑通一条完整链路的全过程摊开讲:从参数怎么定、IP 怎么配、顶层怎么连、激励怎么造,到仿真里怎么比对、上板后怎么定位,一条线走完。适合手上有 FPGA 工程、需要在链路层加一段信道编码,或者正在被维特比译码延迟和握手时序折磨的人看。哪怕你之前没碰过 IP Catalog,只要会写点 Verilog、能看懂 testbench,照着做完也能跑出正确结果。

1. 先把需求拆开:这条链路到底要解决什么问题

1.1 一个比特都不许错的场合,冗余从哪里来

无线链路、光模块、存储回读、长线缆传输,这些场景的共同点是误码率不会为零。物理层给不了你零误码,能给的只是一个统计意义上的概率,比如 1e-3、1e-5。想把这个概率压到 1e-9 以下,最直接的办法就是加冗余,让接收端有能力从残缺的码字里把原始信息推回来。卷积编码配合维特比译码就是这套思路里最经典、也最“工程师友好”的一种:编码端用一个有限长度的移位寄存器,把信息比特和当前状态搅在一起,输出比输入多出若干倍的比特;译码端不靠“枚举所有可能的原始比特串”,而是用动态规划在网格图上走一遍,把最可能的那条路径找出来。

选它的理由很实际。第一,实现代价可控,编码器本质上就是一个移位寄存器加若干异或门,几十个 LUT 的事;第二,纠错增益明确,K=7、码率 1/2 的经典配置在软判决下能拿到五六个分贝的编码增益,这在链路预算吃紧的时候是救命的;第三,工具链成熟,Vivado 里直接有对应的 IP 核可以拉下来配,不用从零写状态机和回溯 RAM。

但“成熟”不等于“拿来就能用”。这两个 IP 核的配置项加起来几十个,彼此之间存在强耦合:约束长度决定生成多项式的位数,码率决定接口位宽,打孔模式决定每个时钟周期有效比特数,回溯深度决定端到端延迟和译码质量。任何一项对不上,表现形式都是“看起来在跑,结果全错”,而且不会报错。

1.2 为什么不自己手写状态机

我一开始也是这么想的:K=7 的卷积编码器,移位寄存器加 8 个异或,一个 always 块就写完了,何必用 IP。后来发现译码端才是真正的地狱。维特比译码的核心是加比选(ACS):每个时钟要算 2^(K-1) = 64 个状态的路径度量,每状态还要比较两条入边的距离、选小的、存下回溯比特。64 个状态并行做加比选,再配上回溯 RAM 和管理逻辑,手写一版能跑通大概要两周,调时序再两周,中间任何一个分支度量计算错一位,输出就是乱的,而且极难调试。

用 IP 核的价值不在于算法多高深,而在于它把那些容易出错的细节——分支度量归一化处理、回溯指针的存储组织、打孔模式下的软信息重组——都封装好了,你只需要保证参数一致、时序正确。代价是要接受它的接口约定和一些不那么直观的配置项。这笔交易我认为是划算的,尤其是在项目排期紧张的时候。

1.3 这套链路适合什么样的项目

说人话:只要你的工程里有一段数据要经过“不可靠信道”,并且对误码率有硬指标,这套东西就用得上。具体到 FPGA 内部,它可能是这样的形态:ADC 采样进来的数据经解调后送到译码器;或者两块板子之间用高速串行口连着,链路层加一段编码;再或者是自己在片内做可靠性实验,往数据里人为注错,看纠错能力。

反过来说,如果你只是想让数据可靠传输,而链路上本身有重传机制、误码率又不高,那加这一层可能就是过度设计。卷积码的编译码延迟不算低(后面会讲回溯深度带来的几十个时钟周期延迟),吞吐也会被码率砍掉一半。所以先想清楚:你到底是缺编码增益,还是缺别的什么。

2. 编码端与译码端的“参数共识”清单

2.1 约束长度、码率与生成多项式

这三个是一组,必须整组对齐。约束长度 K 指的是编码器的移位寄存器级数,它决定了当前输出受多少个历史比特影响,也直接决定译码器的状态数是 2^(K-1)。K 越大纠错能力越强,但译码复杂度指数上升:K=7 是 64 状态,K=9 就是 256 状态,资源占用和时序压力完全是两个量级。

码率是输入比特数与输出比特数之比。码率 1/2 意味着每送 1 个信息比特,出 2 个编码比特;1/3 就是出 3 个。码率越低冗余越多、纠错越强,但同样带宽下吞吐越低。

生成多项式是最容易翻车的地方。它是用八进制或二进制表示的抽头位置,决定移位寄存器里哪几级参与异或。经典的 K=7、码率 1/2 配置,生成多项式是 171 和 133(八进制),分别对应 1111001 和 1011011。这里有两个坑:一是进制的辨识,IP 界面里填数值的地方不会大声提醒你它要八进制还是十进制;二是顺序,第一个多项式的输出对应输出码字的第一个比特,第二个对应第二个比特。两者一颠倒,编码结果完全变样,译码端如果能配置成同样的颠倒顺序,理论上还能对上,但只要有一端没颠,输出就是全错。

我的做法是:配完 IP 之后,用一组已知的短序列(比如 32 个比特)手动用参考模型算一遍,再拿仿真结果逐个比对。这一步花五分钟,能省下后面两小时的抓瞎。

2.2 打孔模式:省带宽但增加对齐难度

打孔(Puncturing)的思路是:编码器照样输出 2 个比特,但按固定模式周期性地扔掉其中一些,这样等效码率就提高了。比如 1/2 码率按 3/4 模式打孔,每 2 个输入比特只保留 3 个编码比特,等效码率变成 3/4,带宽省下来了,代价是纠错能力下降。

IP 核支持打孔,但我给所有第一次搭这条链路的人的建议是:第一版千万别开。原因是打孔会让每个时钟周期的有效比特数变得不规则,AXI4-Stream 接口上的 tdata 位宽和 tvalid 时序都要重新理解,译码端还要求在被打掉的位置上按约定填入“中性”软信息值——这个约定如果和编码端理解不一致,译码器能跑、不报错、输出看着挺像回事,但 BER 会比不打孔还差。

等链路在无打孔配置下跑通了、结果和参考模型一致了,再回来开打孔,并且一定要重新跑一遍完整比对。顺序别反。

2.3 硬判决还是软判决

译码器输入分两种。硬判决就是每个编码比特送 1 位,0 或 1,相当于只告诉译码器“我觉得这是 0”。软判决送多位的量化值,比如 3 位,除了方向还带上了“我有多确信”。软判决的信息量更大,同样的码率下能多拿约 2 dB 的增益,代价是接口位宽变成 n 倍,而且软信息的量化方式(哪个值代表最强的 0、哪个代表最强的 1、中间怎么分布)必须和调制端约定好。

做片内回环验证的时候,直接用硬判决就够了,接线简单,出错也容易定位。等到要做真实的解调链路、或者要测 BER 曲线的时候再上软判决。软判决位宽一般取 3 到 5 位,再高增益提升就很不明显了,纯属浪费。

2.4 回溯深度与端到端延迟

维特比译码器不是收到第一个比特就能吐出第一个比特的。它必须攒够一定长度的数据,才能确定最早的输出比特到底该判成什么,这个攒的长度就是回溯深度(Traceback Length)。深度太浅,早期比特的判决不可靠,纠错性能打折;深度太深,延迟变大、内部存储变大。

经验值在 5 倍的约束长度左右,K=7 就是 35 上下,很多实现会取到 40 或者 48。IP 核里这个参数是可以配的,我建议先用 IP 默认值跑通,再去微调。

延迟这件事在系统层面必须提前算清楚。假设时钟 100 MHz,回溯深度 40,那单纯译码延迟就是 400 ns,再加上接口握手、流水线寄存器,实际可能到 600 ns 以上。如果你的系统里有基于时间戳的对齐逻辑,这个延迟必须在预算里留出来,否则会出现“数据是对的,但对不上帧”的诡异现象。

2.5 接口位宽与 AXI4-Stream 握手

两个 IP 核都用 AXI4-Stream 风格的接口,信号名字是 tdata、tvalid、tready、tlast 这几件套。这里有个非常实际的问题:tdata 的位宽不是你想当然的那个数。

编码器侧,输入 tdata 的位宽通常是 1 位,也就是每个时钟周期送一个信息比特;输出 tdata 的位宽等于码率分子 n,比如 1/2 就是 2 位。译码器侧反过来:输入 2 位(硬判决)或 2×软判决位宽,输出 1 位。这些数字在 IP 配置界面的接口信息里都会显示,但很多人配完就直接例化,靠“猜”来连,结果综合报位宽不匹配,或者更糟——位宽恰好能连上,但数据错位。

我的习惯是:配完 IP,打开它的例化模板,把每个端口的位宽抄到一张纸上,然后在顶层例化时逐个对一遍。这个动作很土,但真的管用。

2.6 尾比特 flush 的约定

这一条是新手最容易忽略的,也是我在实际项目里踩过最深的坑。

编码器处理完一帧最后几个比特之后,移位寄存器里还残留着 K-1 个比特的状态。如果不把这部分信息也编出去,译码器在回溯最后几个符号时就缺少约束条件,导致帧尾的若干比特判决错误。标准的做法是在帧尾补 K-1 个 0(叫尾比特),让编码器状态归零,同时也把这些 0 编码后一起发送。

对应到 IP 核上:编码器在 tlast 之后会自动输出 flush 比特,这部分也要接到译码器去;译码器那边则需要在输入序列末尾看到这些额外比特,才能完整输出整帧。如果你发现“前面几百个比特都对,最后几个比特总是错”,八成就是这个原因。

下面的表格是我每次开新工程时都会填一遍的参数对照表,左右两列必须一字不差:

参数项编码端取值译码端取值不一致的后果
约束长度 K77状态数错,输出全乱
码率1/21/2分组边界错位
生成多项式171, 133(八进制)171, 133(同顺序)输出比特顺序反,全错
打孔关闭关闭打孔位置误判,BER 更差
判决方式不涉及硬判决位宽不匹配,无法连接
输入 tdata 位宽12(硬判决)位宽不匹配或数据错位
输出 tdata 位宽21同上
回溯深度不涉及35~48帧尾误码或延迟超预算
尾比特帧尾补 K-1 个 0接收并参与译码帧尾若干比特错误

3. Vivado 里把两个 IP 核配起来

3.1 工程建立与 IP 添加

先建一个普通 RTL 工程,器件选你板子上实际用的那颗。做功能验证的话,7 系列的中小容量器件就够,两个 IP 核加上测试逻辑占不了多少资源。

工程建好之后,在 Flow Navigator 里打开 IP Catalog,搜索框里敲 encoder 或者 viterbi,能直接找到对应的 IP。双击打开配置界面,这里有个细节:IP 配置对话框顶上会有个“Component Name”,默认是带版本号的名字,建议改成有意义的名字,比如 conv_enc_0 和 viterbi_dec_0,并且把版本号从名字里去掉——后面如果 IP 升级,例化代码不用跟着改。

配置完成点 OK,会弹出一个对话框问你要不要生成例化模板、要不要立即生成输出产物。两个都勾上。生成的例化模板在 Sources 窗口里能找到,就是那个 inst 后缀的文件,直接复制到顶层里改名字用,比手敲端口名可靠得多。

3.2 Convolution Encoder 的逐项配置

打开编码器的配置界面,主要就那么几项:

约束长度:填 7。这个值和生成多项式的位数必须匹配,K=7 就要求每个生成多项式是 7 位二进制。

码率:选 1/2。下拉框里通常有 1/2、2/3、3/4、1/3、1/4 等选项。注意有些版本把打孔后的等效码率也列在这里,选之前看清楚是不是要走打孔路径。

生成多项式:填 171 和 133。这里要特别注意界面上的进制提示,有些版本默认按八进制解析,有些按十进制。填完别急着点 OK,先用界面上的“Example”或者文档里的例子核一遍:输入一串全 0,输出应该也是全 0;输入 1 后面跟一串 0,输出的前 K 个码字应该和手算一致。

打孔:第一次选 Disable。

接口选项:AXI4-Stream 的 tdata 位宽一般不用手动填,它由前面的码率自动推导。配置界面里会显示“Input Data Width”和“Output Data Width”,把这两个数字记下来,后面例化要用。

配完之后点 OK 生成,Vivado 会跑一遍 IP 的定制化流程,可能花几十秒。生成完在 Sources 里能看到 IP 的 .xci 文件,双击可以随时回来改。

3.3 Viterbi Decoder 的逐项配置

译码器的配置界面项更多,逐一说:

约束长度和码率:必须和编码端完全一致,7 和 1/2。

生成多项式:同样填 171 和 133,顺序也要和编码端一致。这个地方我建议不要“顺手换个顺序试试”,除非你已经在编码端也换了,否则必然出错。

判决方式:选 Hard Decision。软判决的选项通常叫 Soft Decision Width,从 1(硬判决)往上选,第一次调试就选 1。

回溯深度:先用默认值,或者手填 35。跑通之后再拿 24、35、48 各试一次,看误码率曲线怎么变。

打孔:Disable。

实现架构:IP 通常会提供一个实现策略选项,大致是速度优先、面积优先、混合三种。第一次调试选默认,别折腾。

输出错误标志:译码器输出侧一般会带一位错误指示信号,挂在 tuser 上或者单独一根线上。这一位不是精确的误码位置指示,更像“本次回溯过程中检测到了异常路径”的粗粒度标志,用来做误码计数很有用,但别拿它当精确 BER 统计。

配完记得看接口信息,把输入输出 tdata 的位宽抄下来。硬判决、码率 1/2 的情况下,输入是 2 位,输出是 1 位。

3.4 配置完之后先做一次位宽对账

这一步是我强制自己做的。打开两个 IP 的例化模板,把端口列表并排放,逐条核对:

  • 编码器输出 tdata 位宽是否等于译码器输入 tdata 位宽(2 = 2)
  • 译码器输出 tdata 位宽是否等于你期望的信息比特位宽(1 = 1)
  • tlast、tvalid、tready 是否都在,是否需要接常量
  • aresetn 是不是低有效复位,时钟使能 aclken 要不要接 1

如果这里发现对不上,别急着在中间插位宽转换逻辑——先回去看配置,因为位宽对不上通常意味着某个参数填错了,硬接转换器只会把错误藏起来。

4. 顶层连线与完整仿真流程实操

4.1 顶层例化的写法

我习惯把两个 IP 串成一个回环模块,输入是信息比特,输出是译码后的比特,中间不加任何额外处理。这样仿真的时候输入输出可以逐位比对,最直观。

module fec_loopback #( parameter integer FRAME_BITS = 1024 )( input wire aclk, input wire aresetn, // 信息比特输入 input wire s_valid, output wire s_ready, input wire s_bit, input wire s_last, // 译码输出 output wire m_valid, input wire m_ready, output wire m_bit, output wire m_last, output wire m_err ); // ---- 编码器输出 ---- wire [1:0] enc_tdata; wire enc_tvalid; wire enc_tready; wire enc_tlast; conv_enc_0 u_enc ( .aclk (aclk), .aclken (1'b1), .aresetn (aresetn), .s_axis_data_tdata ({1'b0, s_bit}), .s_axis_data_tvalid(s_valid), .s_axis_data_tready(s_ready), .s_axis_data_tlast (s_last), .m_axis_data_tdata (enc_tdata), .m_axis_data_tvalid(enc_tvalid), .m_axis_data_tready(enc_tready), .m_axis_data_tlast (enc_tlast) ); // ---- 译码器 ---- viterbi_dec_0 u_dec ( .aclk (aclk), .aclken (1'b1), .aresetn (aresetn), .s_axis_data_tdata (enc_tdata), .s_axis_data_tvalid(enc_tvalid), .s_axis_data_tready(enc_tready), .s_axis_data_tlast (enc_tlast), .m_axis_data_tdata ({m_bit}), .m_axis_data_tvalid(m_valid), .m_axis_data_tready(m_ready), .m_axis_data_tlast (m_last), .m_axis_data_tuser (m_err) ); endmodule

这段代码里有几个点值得单独说。

aclken直接接 1'b1,这在功能验证时没问题,但上板之后如果要做时钟使能控制,记得把它接到真正的使能信号上,并且要保证两个 IP 的使能同步——一个在跑一个停着,握手会卡死。

编码器输入的s_axis_data_tdata我用了{1'b0, s_bit},因为它的位宽是 1 位,但有些版本的模板会把 tdata 声明成更宽、只用最低位。这种情况一定要看模板,别按自己的想象接。

译码器的m_axis_data_tuser接出来的错误标志,在回环验证时可以不管,但在实际链路里接个计数器统计一下很有价值。

4.2 激励从哪来:写一个参考模型

仿真能不能发现错误,取决于你的激励够不够“刁”。全 0 的输入是最没用的——编码出来是全 0,译码出来也是全 0,哪怕参数全错也可能碰巧对上。我一般用伪随机序列加几组固定模式(比如 1010 交替、全 1、单个 1 后面跟一串 0),覆盖面比较全。

参考模型用 Python 写最省事:

K = 7 G = [0o171, 0o133] # 八进制 def conv_encode(bits): state = 0 out = [] for b in bits: reg = (b << (K - 1)) | state sym = 0 for g in G: parity = bin(reg & g).count('1') & 1 sym = (sym << 1) | parity out.append(sym) state = reg >> 1 return out import random random.seed(1) info = [random.randint(0, 1) for _ in range(1024)] info += [0] * (K - 1) # 补尾比特 coded = conv_encode(info) with open('enc_in.mem', 'w') as f: for b in info: f.write(f"{b}\n") with open('dec_ref.mem', 'w') as f: for b in info[:1024]: # 只比对有效信息位 f.write(f"{b}\n") with open('coded_ref.mem', 'w') as f: for s in coded: f.write(f"{s:02b}\n")

coded_ref.mem是给“只测编码器”时用的,用来单独验证编码端配置对不对。这一步很重要:先把编码器单独跑通,再串上译码器,出问题的时候你才知道是哪一端。

4.3 testbench 的读写与逐位比对

testbench 的结构很朴素:读文件、按时钟打拍、写文件、比结果。

`timescale 1ns / 1ps module tb_fec; reg aclk = 0; reg aresetn = 0; always #5 aclk = ~aclk; reg [7:0] info_mem [0:2047]; reg [7:0] ref_mem [0:2047]; reg s_valid = 0; wire s_ready; reg s_bit = 0; reg s_last = 0; wire m_valid, m_bit, m_last, m_err; reg m_ready = 1; integer fin, fout; integer i, err_cnt; fec_loopback u_dut ( .aclk(aclk), .aresetn(aresetn), .s_valid(s_valid), .s_ready(s_ready), .s_bit(s_bit), .s_last(s_last), .m_valid(m_valid), .m_ready(m_ready), .m_bit(m_bit), .m_last(m_last), .m_err(m_err) ); initial begin $readmemb("enc_in.mem", info_mem); $readmemb("dec_ref.mem", ref_mem); fout = $fopen("dec_out.mem", "w"); repeat (10) @(posedge aclk); aresetn = 1; repeat (10) @(posedge aclk); // 送数 for (i = 0; i < 1024; i = i + 1) begin @(posedge aclk); s_valid <= 1; s_bit <= info_mem[i][0]; s_last <= (i == 1023); @(posedge aclk); while (!s_ready) @(posedge aclk); end @(posedge aclk); s_valid <= 0; s_last <= 0; // 收数 err_cnt = 0; i = 0; while (i < 1024) begin @(posedge aclk); if (m_valid && m_ready) begin $fwrite(fout, "%b\n", m_bit); if (m_bit !== ref_mem[i][0]) err_cnt = err_cnt + 1; i = i + 1; end end $fclose(fout); $display("TOTAL_BITS=%0d ERRORS=%0d", i, err_cnt); if (err_cnt == 0) $display("PASS"); else $display("FAIL"); $finish; end endmodule

有两个细节容易写错。一是while (!s_ready) @(posedge aclk);这个等待的位置,如果写在驱动之后,会导致重复驱动同一个比特,帧长度就乱了;正确的位置是在驱动之后等握手完成,再进下一轮循环。二是收数循环的边界,译码器有延迟,必须等 m_valid 拉起来才算数,不能简单地按拍数收。

4.4 跑仿真、看波形、逐段定位

第一次跑仿真,我一般会把运行时间设得比较长,比如 200 us,让整帧跑完。跑完先看控制台那行TOTAL_BITS=... ERRORS=...,数字对得上就成功一半了。

如果 ERRORS 不为零,别急着改 IP 参数,先分三步定位。

第一步,只跑编码器。把译码器旁路掉,把编码器的输出接到一个简单的比较逻辑上,和coded_ref.mem逐位比。这一步能把“生成多项式顺序、进制、码率”这几类问题一次性排除掉。

第二步,把译码器的输入直接灌入参考模型算出来的coded_ref.mem,绕过编码器。如果这样能正确译码,说明译码器配置没问题,问题出在两端之间的连接或时序上。

第三步,看波形的 tlast。译码器的 tlast 应该在整帧最后一位有效数据时拉高,如果提前或者延后,说明尾比特处理有问题。

波形上还有一点值得看:编码器的 m_axis_data_tvalid 在整帧里应该是持续拉高的,因为它是纯组合逻辑输出,只要有输入就有输出。如果发现它断续,说明上游的 s_valid 给得不连续,而译码器对输入连续性是有一定要求的,断续可能影响回溯。

4.5 加噪声,粗测一下纠错能力

链路跑通之后,值得再做一步:在编码器和译码器之间插一个注错模块,按一定概率把编码比特翻转,然后统计译码输出的误码数。这个实验不复杂,二三十行 Verilog 的事,但能让你对“这段码到底有多能纠错”有一个直观认识。

做法是用一个伪随机数发生器,比如 16 位线性反馈移位寄存器,把低几位和某个阈值比较,小于阈值就翻转一位。阈值对应翻转概率,从 1e-3 一直往下调,记录每个概率点下译码输出的错误比特数,画出来就是一条粗略的 BER 曲线。

硬判决下 K=7、1/2 码率的曲线,大概在翻转概率 1e-2 附近开始明显劣化,到 3e-2 左右基本失效;换成软判决,这条曲线会右移一截。这些数字不精确,但足以让你在项目评审时说出“这个配置能扛多差的信道”。

5. 踩过的坑与排查手册

5.1 现象到原因的速查表

下面这些是我自己和身边人真实遇到过的,按“看到什么现象”来组织,出问题的时候直接对着找:

现象最可能的原因处理办法
仿真输出全 0编码器输入一直为 0,或 aresetn 没释放检查激励文件路径、复位时序
输出比特全反生成多项式顺序颠倒两端同时交换或同时不改
前几十位对、后面全乱码率或约束长度不一致对照参数表逐项核
每帧最后几个比特错尾比特没送或没参与译码帧尾补 K-1 个 0 并确认 flush
译码器吐数比预期晚很多回溯深度设置过大调小到 35 附近对比
综合报位宽不匹配tdata 位宽按想象连了查 IP 模板实际位宽
握手卡死,valid 拉高但不前进ready 没接、或一侧 aclken 停了检查 ready 链路与使能
上板后偶发误码,仿真正常跨时钟域或时序未收敛查 CDC、看时序报告
换一组数据就错输入里有全 0 段导致状态退化用伪随机激励重测

5.2 几个印象最深的现场

有一次,仿真怎么跑都对,上板之后误码率一直在 1e-4 上下晃。查了两天,最后发现是编码器和译码器用了两个不同的时钟域,中间没有任何同步逻辑。仿真里两个时钟相位固定,所以看不出问题;上板之后相位漂移,tvalid 和 tready 偶尔采到亚稳态,就出错了。后来把整条链路统一到单时钟域,问题消失。

还有一次,一帧数据的前 900 多位都对,就最后十几位错。查了半天参数,最后发现是帧尾的 flush 比特只送到了编码器,没接到译码器——因为顶层例化的时候,编码器的输出 tlast 我接了,但 flush 之后多出来的那几个周期没有正确处理,被下游的 valid 逻辑吞掉了。这个问题的隐蔽之处在于:错的位置很靠后,容易被误判成“回溯深度不够”。

第三个坑是进制。IP 配置界面上的生成多项式输入框,我下意识按十进制填了 171 和 133,实际应该是八进制的 171(十进制 121)和 133(十进制 91)。填完之后编码器的输出和参考模型怎么都对不上,我一度怀疑是自己参考模型写错了。后来把参考模型的八进制改成十进制再跑,才发现两边一直是一致的——只是都错。这件事之后我养成了一个习惯:参考模型里显式写0o171这种前缀,绝不靠记忆。

5.3 上板之后才暴露的问题

仿真环境是理想化的,上板之后还有几类问题会冒出来。

第一类是复位。IP 核的 aresetn 是低有效,而且要求至少保持若干个时钟周期的低电平。如果复位脉冲太窄,IP 内部状态机可能停在中间态,表现为“有时候能跑,有时候不能”。我的做法是加一个复位展宽逻辑,保证至少 16 个时钟周期的复位。

第二类是时序。维特比译码器的加比选逻辑组合路径比较长,如果时钟频率拉得高,容易成为关键路径。上板前先看时序报告,把这条路径的裕量单独确认一下,必要时降低时钟频率跑一版对照。

第三类是数据源。仿真的激励是文件里读出来的,速率可控;上板之后数据源可能是 ADC 或者串行接口,如果上游给的 tvalid 不连续,或者两帧之间间隔太短导致 tlast 粘连,译码器就可能把两帧当成一帧处理。这类问题的表现是“偶尔整帧报废”,排查的时候要在 tlast 上加一个计数器,看帧数对不对。

6. 资源、时序与后续扩展

6.1 资源占用与架构选择

K=7、码率 1/2 的维特比译码器,资源量级在千级 LUT 加若干块 BRAM,具体数字随实现架构浮动很大。编码器那边几乎可以忽略,几十个 LUT 就够。

如果资源紧张,有两个方向可以调:一是降低约束长度,K=5 的状态数只有 16,资源能砍一大截,代价是纠错能力弱一些;二是在 IP 里选面积优先的实现架构,速度会掉,但对吞吐要求不高的场合完全够用。

反过来,如果吞吐要求高,比如要跑几十兆符号每秒,那就得考虑并行结构——要么用多个译码器核交织处理,要么选支持更高时钟频率的架构。这种情况下 IP 的配置项会多出不少,建议先在小规模上验证思路,再放大。

6.2 时序收敛的几个着力点

这条链路里最容易出问题的是译码器的加比选部分。几个实操建议:

把两个 IP 的时钟频率先设低一点,比如 50 MHz 跑通功能,再逐步往上加,每次加完看时序报告里 WNS 和 WHS 的变化。如果发现某条路径裕量为负,先看它是不是跨了 IP 边界——IP 内部的路径你改不了,但边界上的路径可以加流水。

输入输出各加一级寄存器,能显著改善接口时序。尤其是 tready 这种反向信号,从译码器回到编码器可能路径很长,中间插一级寄存器会舒服很多,代价是握手延迟增加一个周期。

如果用了跨时钟域,务必加异步 FIFO 或者至少两级同步器。前面提到的那个上板误码的例子,就是栽在这上面。

6.3 往后可以怎么扩展

这套链路跑通之后,能往上加的东西不少。

想做更完整的信道仿真,可以在中间插一个 AWGN 噪声发生器,用软判决输入,把 BER 曲线测得更接近理论值。这一步的难点在于软信息的量化映射,编码端算出 0/1,要转成对应的软值,映射关系弄反了增益会全丢。

想做更高速率,可以开打孔把码率提到 3/4 甚至 7/8,但一定要在无打孔版本稳定之后再动。开打孔之后要重新跑一遍完整比对,并且专门测试“打孔位置对齐”这一项。

想做级联码,常见的是把卷积码当内码,外面套一层分组码或者交织器。交织的作用是把突发错误打散成随机错误,因为维特比译码对突发错误特别敏感——连续几十个比特错,回溯路径会被整体带偏。加了交织之后,同样的信道条件下性能会有明显改善,代价是额外的存储和延迟。

最后提一个容易被忽略的扩展方向:把译码器输出的错误标志接一个计数器,统计每帧的错误事件数。这个数字虽然不精确,但作为链路质量的长期监测指标非常好用——它能告诉你信道是在变好还是变差,而不需要停掉业务去做专门的测试。我在几个项目里都保留了这个计数器,接到寄存器上,上位机定期读一下,比什么日志都直观。

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

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

立即咨询