简介:面向FPGA数字通信学习者的8b10b编解码Verilog实现方案,聚焦高速串行数据传输中的直流平衡问题,适合已掌握Verilog基础、希望理解编解码完整数据通路的学生与工程人员。设计按默认编码、差异度计算、编码校正、并串转换与显示五个模块划分,配合Modelsim功能仿真与Quartus II综合适配,最终在Cyclone IV E芯片上完成板级测试。资源包共137个文件、约3.88MB,以12个Verilog源文件与Quartus工程配置为主体,另含综合布局布线生成的cdb、hdb、ddb等中间数据库、rpt报告及readme说明,工程可直接打开复用。已有3767人学习下载,可据此对照理解8b10b码表映射、运行差异度计算与编码校正逻辑,观察并串转换和显示模块的衔接方式,体会编解码电路的分层组织,借助现有工程快速搭建仿真环境、复现板级验证流程与调试思路,适合作为课程设计或入门项目的参考底稿。
1. 8b10b不是玄学:FPGA里那套编码到底在解决什么
第一次在 FPGA 上做高速串行链路的人,看到 8b10b 常会觉得这是某种玄学——10 个比特里只塞 8 个比特,剩下 2 个比特凭空“浪费”掉。但只要做过千兆以太网、PCIe、SATA 或者自研高速背板,就会发现这套编解码几乎绕不开。它解决两个硬问题:保证 10bit 码流里有足够的电平跳变,让接收端 CDR 能持续恢复时钟;维持 DC 平衡,让 AC 耦合链路不产生基线漂移。这份基于 Verilog 的 8b10b 编解码实现,把编码器、解码器、RD 演进和错误检测拆成可综合、可仿真、可上板的模块,配合 Quartus 工程能直接跑通编译和板级验证。适合正在做高速串行接口原型、或者想把 8b10b 从“看懂表”推进到“写出来并跑通”的从业者。
2. 编码原理与表结构:5b/6b + 3b/4b 如何把 256 项压到 40 项
2.1 DC 平衡与跳变密度:8b10b 存在的两个硬理由
串行链路上如果连续发送长串 0 或长串 1,接收端的时钟恢复电路会失去参考沿,PLL 开始自由振荡,采样点漂移,误码随之而来。8b10b 的第一个作用就是限制游程长度:任何合法码字映射后,连续相同比特不会超过 5 个,接收端每 5 个 UI 内至少能看到一次跳变。第二个作用是 DC 平衡,编码后的 10bit 码字中 1 和 0 的数量差被限制在极小范围内,且通过 RD 机制在码流级别持续纠偏,避免变压器或电容耦合链路积累直流偏置。
8b10b 还有额外价值:大量非法码字天然不存在,解码端可以据此判断链路是否出错。256 个数据字节加上 12 个控制字符,映射到 1024 个可能的 10bit 码字里,合法码字只占一小部分,剩下的都是非法码,这是链路层做误码监控的基础。
如果把 8bit 直接映射到 10bit,需要 256×2 张表项,组合逻辑会非常庞大。8b10b 的设计精髓是把 8bit 拆成低 5 位和高 3 位,分别用 5b/6b 和 3b/4b 两张小表处理,再用一位 RD 状态把两张表串起来。
2.2 RD 演进:一位状态位决定整张表选哪一列
RD(Running Disparity)是 8b10b 的核心状态,只有一位,表示当前码流的极性倾向。标准里的定义是:RD 为正表示最近的非零 disparity 是正的(1 比 0 多),RD 为负则相反。编码每个字节时,根据当前 RD 选择 5b/6b 表和 3b/4b 表中对应的列,编码完成后再根据码字的 1 的个数更新 RD。
规则可以归纳成三条:码字里 1 的个数为 4(对 6bit)或 3(对 4bit)时,RD 往正方向走;1 的个数为 2(对 6bit)或 1(对 4bit)时,RD 往负方向走;1 和 0 各占一半时,RD 保持不变。5b/6b 先算出一个中间 RD,3b/4b 再基于这个中间 RD 选择码字并算出最终 RD。
这里有个容易被忽略的细节:D17.7、D18.7、D20.7 和 K28.7 这四个码字,3b/4b 部分不能直接用常规的 0001/1110,而要用 0111/1000。原因是它们的 5b/6b 部分已经是平衡码,4b 部分必须承担起把 RD 推回正确方向的责任。如果漏掉这条例外,编码器在特定数据序列下会出现 RD 漂移,解码端报 disparity error,链路时通时断,排查起来非常痛苦。
2.3 用脚本把标准表生成 Verilog case
手工抄 32 行 5b/6b 表和 8 行 3b/4b 表很容易出错,而且一旦抄错一行,仿真可能刚好不覆盖,上板后才暴露。我一般用一段 Python 脚本把表数据维护在字典里,同时生成 Verilog case 和参考模型,两边比对。
# gen_8b10b.py # 5b/6b 表:key 为 5bit 输入,value 为 (RD- 码字, RD+ 码字) T5B6B = { 0b00000: (0b100111, 0b011000), 0b00001: (0b011101, 0b100010), 0b00010: (0b101101, 0b010010), 0b00011: (0b110001, 0b110001), 0b00100: (0b110101, 0b001010), 0b00101: (0b101001, 0b101001), 0b00110: (0b011001, 0b011001), 0b00111: (0b111000, 0b000111), 0b01000: (0b111001, 0b000110), 0b01001: (0b100101, 0b100101), 0b01010: (0b010101, 0b010101), 0b01011: (0b110100, 0b110100), 0b01100: (0b001101, 0b001101), 0b01101: (0b101100, 0b101100), 0b01110: (0b011100, 0b011100), 0b01111: (0b010111, 0b101000), 0b10000: (0b011011, 0b100100), 0b10001: (0b100011, 0b100011), 0b10010: (0b010011, 0b010011), 0b10011: (0b110010, 0b110010), 0b10100: (0b001011, 0b001011), 0b10101: (0b101010, 0b101010), 0b10110: (0b011010, 0b011010), 0b10111: (0b111010, 0b000101), 0b11000: (0b110011, 0b001100), 0b11001: (0b100110, 0b100110), 0b11010: (0b010110, 0b010110), 0b11011: (0b110110, 0b001001), 0b11100: (0b001110, 0b001110), 0b11101: (0b101110, 0b010001), 0b11110: (0b011110, 0b100001), 0b11111: (0b101011, 0b010100), } # 3b/4b 表:Dx.7 的例外在 Verilog 里单独处理 T3B4B = { 0b000: (0b0100, 0b1011), 0b001: (0b1001, 0b1001), 0b010: (0b0101, 0b0101), 0b011: (0b0011, 0b1100), 0b100: (0b0010, 0b1101), 0b101: (0b1010, 0b1010), 0b110: (0b0110, 0b0110), 0b111: (0b0001, 0b1110), } def popcount(x): return bin(x).count("1") def next_rd(rd, code, width): ones = popcount(code) if ones == width // 2: return rd return 1 if ones > width // 2 else 0 def gen_case(var, table, win, wout): lines = [f" case ({var})"] for k in sorted(table): neg, pos = table[k] kb = format(k, f"0{win}b") nb = format(neg, f"0{wout}b") pb = format(pos, f"0{wout}b") lines.append(f" {win}'b{kb}: dout = rd ? {wout}'b{pb} : {wout}'b{nb};") lines.append(" default: dout = 0;") lines.append(" endcase") return "\n".join(lines) print(gen_case("din5", T5B6B, 5, 6)) print(gen_case("din3", T3B4B, 3, 4))这段脚本做了三件事:把码表维护成 Python 字典,避免手写 Verilog 时抄错;提供next_rd参考实现,方便和仿真结果对照;直接输出可粘贴进 Verilog 的 case 语句。参数win和wout分别控制输入输出位宽,rd ? pos : neg里的三目运算符对应 RD 正时选 RD+ 列。跑一遍脚本,把输出贴进 RTL,再写一个小 testbench 用 Python 算出的期望值做比对,表的正确性就能闭环。这套流程在后面调解码器和排查 RD 漂移时还会反复用到。
3. 编码器 Verilog 落地:接口、查表与 RD 流水线
3.1 模块接口与位序约定
编码器模块的接口需要同时考虑数据通路和控制字符。数据侧是 8bit 输入、1bit 的 K 字符标志、1bit 的输入有效;输出侧是 10bit 编码结果、1bit 的输出有效、1bit 的当前 RD 状态。位序约定必须在模块头注释里写死,否则后面接串行器时 10bit 的顺序一颠倒,整个链路就废了。
常见约定是:data_out[5:0]对应 abcdei,data_out[9:6]对应 fghj,data_in[4:0]对应 EDCBA,data_in[7:5]对应 HGF。串行发送时先发 a,所以data_out[0]接串行器的第一级。这个约定和多数 IP 手册一致,但不同厂商的串行器可能按 MSB first 接,落地前必须对一遍手册,别想当然。
module enc_8b10b ( input wire clk, input wire rst_n, input wire din_valid, input wire [7:0] din, input wire din_k, // 1 表示控制字符 output reg dout_valid, output reg [9:0] dout, output reg dout_rd, // 编码后的 RD 状态 output reg dout_k );接口里保留dout_k是为了让下游知道当前传输的是数据还是控制字符。RD
本文还有配套的精品资源,点击获取