ARTICLE DETAIL

资讯详情

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

HDB3编解码的Verilog实现与仿真验证

HDB3编解码的Verilog实现与仿真验证 简介数字通信系统中的线路编码直接关系到传输可靠性HDB3码编解码的Verilog实现是理解该过程的实用素材。工程基于ISE环境完成设计、仿真与综合适合通信工程、电子类专业学生及硬件工程师用于学习线路编码原理并落地FPGA。压缩包内含423个文件、约3.06MB覆盖Verilog源文件、testbench测试平台、ISE工程配置、仿真波形和综合报告包括.v、.prj、.ucf、.bit等类型从RTL代码到比特流一应俱全。目前已有1216人学习下载。除核心编解码逻辑外资料还提供可直接运行的测试环境与工程脚本方便执行仿真、查看波形并了解综合布局流程目录结构清晰模块划分细致能帮助快速定位编码、解码与验证环节减少环境搭建成本。整体可作为课程设计、毕业设计或通信系统FPGA开发的参考工程。 做基带通信的FPGA开发迟早都会碰到HDB3码。如果你和我一样在学校里背过“连续四个零用000V或B00V替换”的规则真到了要用Verilog写编解码时才会发现教材上那一句话远远不够。V脉冲的极性怎么确定、奇偶性状态什么时候清零、解码端怎么把违例脉冲还原成零这些问题不亲手写一遍根本感受不到。这篇文章就基于我自己实现HDB3码编解码的过程把设计思路、关键代码和仿真踩坑一起整理出来希望能帮到正在做基带Verilog设计或者正在赶课设的工程师。1. 为什么是HDB3直流抑制和时钟恢复的真正矛盾1.1 双极性码的初衷先回到最基础的问题。HDB3是由AMI码演变过来的AMI又叫信号交替反转码规则非常简单二进制“1”交替输出正脉冲和负脉冲二进制“0”输出零电平。这样做最大的好处是信号里没有直流分量可以通过变压器耦合到线路上也方便后续的均衡和整形电路处理。工程上很多地方用“双极性”而不是单极性就是为了这个直流特性。单极性不归零码如果出现长串的“1”信号平均电平会明显漂移接收端的判决阈值就不稳了。AMI码用正负交替脉冲把能量推到以载频为中心的频带中低频分量很小这是它在上世纪就大规模用于PCM传输的原因之一。1.2 长连零为什么麻烦AMI码的短板也很明显如果数据里出现连续多个“0”线路上一段时间内完全没有电平变化接收端用来恢复时钟的锁相环就失去参考边沿。PLL一旦长时间没有边沿可锁输出时钟的频率和相位都会漂漂到一定程度采样判决就会出错。早期的解决思路是限制信源编码保证发送数据里“0”的密度不会太低。但这会给上层协议增加很多约束不利于透明传输。于是就有了HDB3这类“高密度双极性”编码它的思想是在必要的时候主动制造一些脉冲让接收端始终有边沿可以用。1.3 HDB3替换规则HDB3在AMI的基础上加了一条当出现连续四个“0”时不直接输出“0000”而是替换成“000V”或“B00V”两种码型之一。具体选哪一种取决于“从上一个替换点开始到当前这四个零之前出现的‘1’的个数”。这里先约定一下极性表示。假设最近的一个非零脉冲是正极性的那么如果“1”的个数是奇数替换成“000V”其中V脉冲的极性与前一个非零脉冲相同。如果“1”的个数是偶数替换成“B00V”其中B脉冲的极性与V相反V脉冲的极性与前一个非零脉冲相同。看起来不复杂但要注意V是一个故意违反“正负交替”规则的违例脉冲所以叫Violation。接收端只要发现两个相邻非零脉冲的极性相同就知道这是人为插入的违例于是把这一小段还原成“0000”。我用一个简单例子说明。假设前一个非零脉冲是正之后出现四个零。如果这之前已经有奇数个“1”就输出“000 ”就是V如果是偶数个“1”就输出“- 0 0 ”其中第一个“-”是B最后一个“”是V。两种情况下V都是正极性跟前面那个正脉冲同极性制造出双极性违例特征。1.4 为什么偏偏是“四个零”有人会问为什么不选三个零或者五个零这是平衡了编码效率和定时恢复能力的结果。选太短会频繁插入替换码型线路上的额外脉冲太多传输效率变差选太长则连续零时间过长定时恢复还是有风险。四个零是一个经过实践检验的折中值后来被写进了ITU-T G.703等标准E1接口的基带部分用的就是HDB3。2. 编码器设计不是把公式背下来就能写对2.1 需要维护的状态量写HDB3编码器之前建议先把状态变量列清楚不要直接开写always块。编码器至少要记住三样东西连续“0”出现了多少个上一个非零脉冲的极性是正是负从上一次替换点开始到当前位置为止“1”的个数是奇数还是偶数。第一个状态用一个计数器就能实现很多初学者会用Verilog计数器去数零的个数这没问题。但第二和第三个状态经常被忽略结果就是编码输出极性错乱接收端解码出来全是乱码。我还有第四个状态要提醒因为替换码型的确定要到第四个“0”出现时才能知道该用“000V”还是“B00V”但“B00V”码型的第一个符号是B不是零。这意味着编码器在输出上需要有“回写”能力不能看到什么就立刻输出什么。2.2 逐bit处理时的隐藏时序问题很多人第一版编码器会写成这样每个时钟进来一个bit如果是“0”计数器加一输出零电平如果是“1”输出交替脉冲计数器清零。这个写法在遇到普通数据时没问题但只要碰到“0000”就麻烦了。第4个“0”进来之前你只知道前面已经有3个零还不知道这4个零的替换类型。可“B00V”要求在第一个零的位置上输出B脉冲如果你前面已经把第一个零的位置输出成零电平等第4个零到来时再去改已经来不及了。解决思路有两个方向。一是把整个编码器改成按4bit分组处理一组一组判断但这会使串行比特流的数据通路割裂后面还要处理帧对齐和组边界。另一个更工程化的做法是给输出加一个固定长度的流水线比如4拍延迟让发送端送出第4个零时仍然有机会改写流水线里的第一个符号位置。实际项目里我推荐加流水线。虽然引入了4个周期的固定延迟但对连续数据流来说延迟固定就完全不是问题接收端解码时也会有对应的延迟只要两边保持一致就行。2.3 极性跟踪的细节极性跟踪是最容易写错的地方。正常“1”脉冲是交替翻转的但V脉冲不翻转。也就是说遇到V之后下一个正常“1”应该跟V的极性相反。这个逻辑如果简单写成“每个非零脉冲都翻转”就会把V也当成普通脉冲翻转破坏极性关系。我习惯单独建一个“violation”标志输出V时极性跟踪寄存器不翻转输出普通1或B时极性跟踪寄存器正常翻转。解码端的违例检测也正是靠“两个相邻非零脉冲不交替”这个特征来完成的。3. 编码器关键代码状态机与四拍输出流水线3.1 顶层与端口定义下面是我自己项目里使用的编码器端口定义为节约篇幅做了简化只保留最核心的主数据通路。三电平符号用2bit表示00是零电平01是正脉冲10是负脉冲。module hdb3_encoder ( input wire clk, input wire rst_n, input wire din, // 输入二进制bit output reg [1:0] sym_out, // 输出三电平符号 output reg sym_valid );这里把三电平粗略量化为2bit实际工程如果后面接的是D/A或者线路驱动可以根据需要调整编码方式但核心逻辑不变。3.2 状态变量和核心逻辑核心状态变量是zero_cnt、last_pole和odd_ones另外加一组4位深度的输出缓冲sym_buf。symbol_buf的作用就是前面说的“回写窗口”当第4个零到来时如果确认替换类型是B00V就修改sym_buf的第一个槽位把这个位置原本应该输出的零改成B脉冲。reg [1:0] zero_cnt; reg last_pole; // 1: positive, 0: negative reg odd_ones; // 1: odd number of 1s since last violation reg [1:0] sym_buf [0:3];每拍的处理逻辑示意如下去掉了valid和反压细节但主要状态更新都在always (posedge clk or negedge rst_n) begin if (!rst_n) begin zero_cnt 2d0; last_pole 1b1; odd_ones 1b0; sym_out 2b00; sym_valid 1b0; end else begin if (din 1b1) begin // 普通“1”脉冲按AMI规则交替输出 zero_cnt 2d0; odd_ones odd_ones ^ 1b1; sym_buf[3] last_pole ? 2b10 : 2b01; // 与前一个极性相反 last_pole ~last_pole; end else begin // 遇到“0” if (zero_cnt 2d3) begin // 连续第4个0触发替换 if (odd_ones 1b0) begin // 偶数个1B00V把缓冲里第一个符号改成B sym_buf[0] last_pole ? 2b10 : 2b01; // B与V相反 end // B00V和000V的最后一个符号都是V sym_buf[3] last_pole ? 2b01 : 2b10; // V与前一个同极性 zero_cnt 2d0; odd_ones 1b0; end else begin zero_cnt zero_cnt 1b1; sym_buf[3] 2b00; // 普通零电平 end end // 每拍滑动一次输出缓冲 sym_buf[0] sym_buf[1]; sym_buf[1] sym_buf[2]; sym_buf[2] sym_buf[3]; sym_out sym_buf[0]; sym_valid 1b1; end end上面的代码严格要求缓冲滑动和写操作在同一拍内完成才有效实际写的时候要注意阻塞赋值和非阻塞赋值的混用最好把“滑动”和“写入”拆开甚至直接用数组索引做组合读出。我这里的写法更接近行为示意方便理解数据流动不是最优的可综合代码但思想是清楚的。3.3 为什么输出延迟可接受有同学看到这段代码会问输入明明是实时进来的输出却延迟了4拍这不算错吗在链路设计里固定延迟完全正常。编码器的输出本来就和输入不是“同一时刻对应同一bit”的关系只要接收端解码时也按相同的延迟去对齐最后恢复出来的二进制序列就完全正确。实际上更严格的电路还会在数据通路里插入多级寄存器来改善时序优化fmax所以固定几拍延迟是FPGA开发里的常规操作。真正不能接受延迟的是那种要求逐bit实时双向回环的场景遇到那种需求通常就要改成并行分组编码而不是串行流式。4. 解码器看违例抓替换把四个零补回来4.1 从三电平符号还原出二进制解码端的任务是反向操作。输入是经过线路传输后的三电平符号输出是二进制bit流。最简单的映射是非零脉冲符号输出“1”零电平输出“0”。但这样还原出来的序列在替换码型的位置上并不是“0000”。比如“000V”会被还原成“0001”“B00V”会被还原成“1001”。所以解码器还必须做第二步检测双极性违例凡是违例位置对应的bit强制改回“0”并且把同一个替换码型里的其他三个位置也改成“0”。4.2 违例检测的判断条件检测违例的依据是非零脉冲的极性是否交替。两个相邻的非零脉冲如果极性相反是普通AMI序列如果极性相同就是人为插入的违例。因此解码器需要记住“上一个非零脉冲是什么极性”。每当收到一个新的非零符号时拿它和上一个非零符号比较极性不同正常“1”输出bit为1极性相同检测到替换当前符号和它前面的三个符号都应该还原成四个零。这里还要注意检测到违例时不只是当前这个V要变成0它前面的三个符号也要变成0。因为HDB3替换总是以4个符号为一组。所以解码器还需要暂时保留最近三个符号对应的“初步解码结果”才能做到整组修正。4.3 解码器代码主体module hdb3_decoder ( input wire clk, input wire rst_n, input wire [1:0] sym_in, output reg dout, output reg dout_valid ); reg [1:0] bit_buf [0:3]; reg [1:0] last_pulse_sym; reg last_pulse_valid; always (posedge clk or negedge rst_n) begin if (!rst_n) begin bit_buf[0] 2d0; bit_buf[1] 2d0; bit_buf[2] 2d0; bit_buf[3] 2d0; last_pulse_valid 1b0; dout 1b0; dout_valid 1b0; end else begin // 第一步按三电平符号粗解出bit bit_buf[0] (sym_in 2b00) ? 1b0 : 1b1; if (sym_in ! 2b00) begin // 当前是非零脉冲 if (last_pulse_valid (sym_in last_pulse_sym)) begin // 双极性违例整组替换成0000 bit_buf[0] 1b0; bit_buf[1] 1b0; bit_buf[2] 1b0; bit_buf[3] 1b0; end last_pulse_sym sym_in; last_pulse_valid 1b1; end // 移位输出 dout bit_buf[3]; dout_valid 1b1; bit_buf[1] bit_buf[0]; bit_buf[2] bit_buf[1]; bit_buf[3] bit_buf[2]; end end endmodule需要提醒的是和编码器一样解码器也引入了四级缓冲延迟。这正好和编码器端的延迟对应起来测试时可以对编码输入和解码输出做完整对齐。5. 仿真验证用波形发现规则里没写清的边界5.1 测试向量怎么设计HDB3编码器最容易出错的地方集中在几类输入上。普通交替数据能过不代表长连零能过。我建议测试平台至少包含下面几组向量普通“01010101”这类交替序列验证AMI极性是否正常交替一段包含4个连续零的序列例如“1100001”验证000V或B00V触发是否正确连续两组四个零中间只有一个“1”验证奇偶性状态是否在替换后正确清零极长的全零序列比如连续40个“0”验证编码器能否持续输出有效脉冲不会因为状态机卡死而丢数据随机码流长度至少几千bit用来做全流程回归。我在仿真时最喜欢用的是“全零加一个1”的测试比如“0000100000001”。如果编码器某个地方状态没清对立刻能看到V/B码型的位置和极性错乱。5.2 测试平台中怎么观察结果编译好DUT之后我会写一个简单的testbench把输入端的数据模型化输出端的符号打印成波形文件然后用ModelSim或者Vivado的仿真器看波形。看波形时重点看几个信号zero_cnt是否按预期从0增长到3、odd_ones是否在替换点被清掉、sym_out会不会在B00V替换后出现连续的相同极性脉冲。当你看到两个连续的非零脉冲极性相同就说明违例产生了如果错误地把V当成普通脉冲翻转这个相同极性特征就消失解码端自然无法检出违例。为了方便自动化检查最好在testbench里再写一个参考模型用软件方式实现同样的HDB3编码把DUT的输出和参考模型逐拍比较。这样可以快速发现是哪一个时钟周期开始不对再去定位是哪段状态机逻辑的问题。这种对比测试在随机码流回归时非常省事。5.3 一个反直觉的波形现象我第一次写测试时遇到过这么一件事编码输出看起来很正常但解码输出和原始输入对不上差了整整几个bit。后来查了半天问题不在编码器本身而是我的测试平台没考虑编码器输出有4拍延迟直接把编码输入和解码输出逐bit比较自然全错。这个问题在教科书上不会专门写但实际调FPGA时特别常见。正确的做法要么在testbench里给参考模型加同样的延迟要么把数据对齐后再比对。后面我在很多工程里也用类似思路处理其他编解码模块凡是带流水线的处理链路验证阶段都要先明确延迟拍数。6. 上板前要留意的几个工程细节6.1 三电平符号和物理接口的映射Verilog里我用2bit表示三电平这只是一种内部抽象。如果真的要接到外部线路驱动器通常还要转换成真正的双极性脉冲甚至经过滤波和变压器耦合。FPGA内部一般只做数字逻辑三电平符号经过IO引脚输出后还要靠外部模拟电路合成为正负脉冲。这里最容易犯的错是符号电平编码不一致。比如编码器输出“01”代表正脉冲“10”代表负脉冲但到了另一个模块解码时却把“10”当成了正脉冲极性一颠倒违例检测就全乱了。建议在顶层模块里用参数把三种状态定义成宏所有模块共用不要各写各的魔数。6.2 时钟速率和数据速率的关系串行HDB3编码是每bit一个时钟节拍数据速率和时钟频率一样。如果要处理更高数据率比如多路E1复用就需要做多bit并行通道。常见做法是先把数据送入一个小FIFO按4bit或8bit一组取出再并行判断替换逻辑。并行版本的HDB3实现起来比串行要复杂一些主要是连续零的判断不再是一个一个数而是要在并行窗口里统计跨边界的连续零个数。我个人的经验是并行实现尽量先把这个串行版状态机跑通再用“串行思想做并行化”去改造不要直接跳到并行代码否则状态量和窗口边界会搅在一起很难调试。6.3 不要忘记异步复位和跨时钟域信号如果这个编解码模块嵌在一个大的系统中前端可能是异步串口或者DDR缓存过来的数据时钟域可能不一致。最简单的处理是在编码器之前加一个异步FIFO做跨时钟域隔离保证送入编码器的bit流和编码器时钟完全同步。输入数据的拉高信号也最好先做两级同步防止亚稳态传到状态机里。同样的解码器输出的bit流如果还要送到下游的串并转换模块也要确认握手信号是否满足对方的时序要求。我自己踩过的坑是FPGA综合时没有任何警告上板跑一段时间后偶发性出现错误bit最后查到是异步FIFO的读使能没有和读时钟对齐导致漏读了一个bit编码序列整体错位。6.4 利用好在线逻辑分析仪仿真通过之后我还会在工程里保留编码器和解码器内部几个关键寄存器的调试信号比如zero_cnt、odd_ones和last_pole_valid上板后用逻辑分析仪抓一下实际运行的波形。这一步非常有用因为仿真里的输入都是理想信号实际链路里可能有毛刺和噪声一旦出现误码光看最终输出不好定位有这几个内部信号就能快速判断是编码端的问题还是接收端物理接口的问题。我自己会把逻辑分析仪触发条件设成“连续两个同极性非零脉冲出现”这样一旦接收端出现违例特征就能抓到大段的上下文波形排查效率高很多。最后分享一个我个人的习惯无论多简单的编解码模块我都坚持先写一个小的验证脚本把编码结果导出成文本再用Python之类的脚本按HDB3规则做离线比对。脚本比对虽然笨但比眼睛看波形可靠得多。做过一次完整的实现和验证之后HDB3的规则就不再是书本上需要背的条文而是一段段能想清楚为什么这样设计的状态逻辑了。本文还有配套的精品资源点击获取
返回列表