
简介面向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 演进一位状态位决定整张表选哪一列RDRunning 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 先算出一个中间 RD3b/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, f0{win}b) nb format(neg, f0{wout}b) pb format(pos, f0{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]对应 abcdeidata_out[9:6]对应 fghjdata_in[4:0]对应 EDCBAdata_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本文还有配套的精品资源点击获取