ARTICLE DETAIL

资讯详情

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

FPGA实战:8b10b编解码原理与Verilog实现

FPGA实战:8b10b编解码原理与Verilog实现 简介一套基于FPGA的8B/10B编解码Verilog实现工程定位面向FPGA学习者与高速串行接口设计人员旨在帮助理解直流平衡编码原理以及编解码器在真实硬件中的落地过程。压缩包共包含137个文件整体约3.88MB其中以Verilog源码、Quartus II工程文件、sof下载文件、仿真脚本及各类报告为主从工程配置到验证结果一应俱全解压后直接打开qpf工程即可运行整个设计流程。目前已有3745人学习下载。设计内部按默认编码、差异度计算、编码校正、并串转换和显示五个模块划分完整覆盖编码、解码与差异度校验环节配套ModelSim功能仿真和Quartus II逻辑综合最终在Cyclone IV E系列的EP4CE6F17C8芯片上完成适配与下载测试。对希望快速掌握8B/10B编解码实现、参照完整FPGA工程结构或从事高速串行传输验证的读者这套资料提供了可直接复用和二次开发的起点。1. 为什么搞8b10b串行链路上的“平衡艺术”做FPGA的同学迟早会碰到8b10b不管你是调PCIE、USB3.0、千兆以太网还是SATA、JESD204B底层物理编码子层里都跑着8b10b或者它的小改款比如64b/66b、128b/130b。说是“迟早”是因为只要链路是串行的、速度一上来DC平衡问题就躲不掉。先说一个最原始的问题为什么不能直接把8bit数据怼到串行线上因为接收端需要从数据流里恢复时钟而恢复时钟依赖信号边沿的密度。如果你的数据是连续的0或者连续的1接收端过一段时间就“失去参考”了CDR时钟数据恢复直接罢工。再一个问题是直流漂移长时间发送同一电平会让线路的直流分量偏移长期运行误码率就上来了。8b10b的核心思想就两个字平衡。8bit数据映射成10bit码字这10个bit里0和1的数量差异被严格控制运行差异度Running Disparity简称RD被限制在正负1之间。同时保证码字里连续的0或1不超过5个这样接收端就有足够多的跳变沿来恢复时钟。再往深一层看8b10b的设计还有一个很多新手容易忽略的点它不是一个简单的查表映射而是分成了3b/4b和5b/6b两级子映射。为什么这么设计因为如果直接做8bit到10bit的完整映射表表规模是2^101024项虽然Verilog里写case也写得下但逻辑综合和时序优化会麻烦不少。分两级之后每一级的逻辑都很小组合逻辑路径短时序更好收敛这也是当年IBM那帮工程师的高明之处。说实话我个人做FPGA项目多年真正对8b10b“开窍”是在自己用Verilog写过一遍编解码并且调通了高速收发器比如Xilinx的GTP/GTX之后。光看协议文档你永远没法真正理解RD状态为什么会对、那几根控制信号为什么要同步、K码为什么是干这个用的。这篇文章就把我手写8b10b编解码模块的完整思路、代码结构、仿真排错过程捋一遍给要做相关项目的朋友一个可以“抄作业”的参考。2. 8b10b编解码核心机制拆解2.1 编码表与RD状态机的关系8b10b编码的输入是一个8bit数据D码加上一个控制位K码使能输出是10bit码字。编码表本身不是随意的映射它做了两个关键约束每个码字中1的个数和0的个数的差值只能是0、2或-2极少数特殊码字除外根据当前RD状态RD或RD-来选择发送码字的极性保证码流平均DC电平为0RD状态机是怎么工作的简单说当前RD为正表示最近发出去的码字里1比0多下一步倾向于发负极性的码字来拉平衡RD为负则相反。具体计算规则如下如果码字里1比0多2个且当前RD则下个码字的RD变为-如果码字里1比0多2个且当前RD-则下个码字的RD变为如果码字里0和1一样多RD保持不变这个规则用Verilog实现就是几行always块的事但前提是你得把编码表的极性分类搞清楚。这里我补充一张常用于快速查找的编码表结构方便大家对照代码理解。表格不是完整128项数据只是把几个典型字符列出来关键看规律。输入数据数据类型RD-时输出RD时输出D0.0 (0x00)D码100111 0100011000 1011D1.0 (0x01)D码011101 0100100010 1011D21.5 (0xB5)D码101010 1010101010 1010K28.5 (0xBC)K码001111 1010110000 0101看到D21.5这行的规律了吗它本身就是一个平衡码0和1各5个所以不管RD是正是负输出都一样。这也是10bit码字里少数能保持完全平衡的字符K28.5则因为自带连续的1和0成为接收端做码组对齐的黄金选择。2.2 控制字符K码到底有什么用K码在8b10b体系里不是用来传数据的它承载的是“命令”或“标记”。协议里一共有12个K码但真正在物理层用得最多的是K28.1、K28.5和K28.7。K28.5常用作comma逗号字符用于接收端做字节对齐。它的10bit码字里有一个唯一的“0011111”或“1100000”模式连续的1或0不会在任何D码组合中出现K28.1和K28.7常用于一些协议特定的控制序列比如PCIE里的SKPskip字符会用K28.0/K28.3等做FPGA实现的时候K码使能信号通常叫K或KCHAR必须在发送端和接收端有统一约定。比如你的链路层数据是32bit还是64bit的K码字节在整个通道里怎么分布这些属于协议适配层的工作物理编码子层只管按输入编码就行。经验之谈新手写编解码时经常忽略K码使能与数据对齐的问题。如果你的数据位宽是8bit那还好办一个字节对应一个使能。但如果用了32bit的链路层接口一个时钟周期4个字节那么你既要判断4个字节各自的K使能还要注意字节顺序对不对。曾经我调一个多通道的项目发送端byte0放K码接收端却按byte3的位置去取comma结果对齐逻辑始终不触发排查了整整一天才发现是字节通道映射的问题。2.3 RDRunning Disparity究竟怎么算RD的计算是8b10b实现里最容易写错的地方。多数资料会告诉你“编码后统计1和0的个数差”听起来很简单但实际编码过程是分两步走的先对低5bit做5b/6b编码根据当前RD算出中间RD再对高3bit做3b/4b编码根据中间RD算出最终RD也就是说RD在整个编码过程中会更新两次。这个细节非常关键如果你的代码先编码完整10bit再统一更新一次RD那么对于某些输入序列结果可能碰巧对但遇到边界情况就会出错。具体的RD更新判断其实用不着真的去数1的个数。更高效的做法是直接分类6bit子码输出为6bit里有“000111”这种三位相同的结构4bit子码输出为4bit里也有类似特征这些特定的模式会直接决定RD翻转方向。分类判断加几个组合逻辑比统计1个数少占用不少LUT。我在实际工程里习惯把RD状态做成一个单独的模块输出给编码器用同时回传给上层状态机好处是调试的时候可以单独看这个信号的变化轨迹定位问题的时候效率极高。3. 编码器Verilog实现架构设计、代码结构与关键点3.1 顶层模块如何划分我建议把8b10b编码做成独立的模块接口尽量标准化方便后续复用到不同项目。顶层端口如下module enc_8b10b ( input wire clk, input wire rst_n, input wire [7:0] data_in, input wire k_in, // 1: K码使能, 0: D码 output reg [9:0] data_out, output reg rd_out // 当前码字发送后的RD状态 );为什么要把RD输出也拉出来因为接收端的解码也需要RD有时候调试系统级回环测试时你需要把发射端的RD和接收端的RD做交叉比对。如果你的模块把这个状态锁在内部不让外面看仿真调试就没法做了。除了顶层我通常会拆三个子模块5b/6b编码器、3b/4b编码器、RD更新逻辑。这样拆的优势是每个子模块的逻辑都很纯粹综合时时序路径短后期如果要改协议参数比如做64b/66b或自定义变种也只需要换其中一小块。有一个设计习惯值得推荐把编码表用case语句罗列清楚但不要把这些case放在组合逻辑块里而是用casez匹配“XX”这种不关心的位。因为8b10b里有几个码字的映射是等价的同一个输入在RD和RD-下会输出两个不同码字写casez可以让综合更清楚哪些分支可以合并优化。3.2 编码器源码核心片段解析这里给出最核心的5b/6b编码逻辑。// 5b/6b encoder reg [5:0] coded_6b; always (*) begin casez ({k_in, data_in[4:0]}) // D码 6b0_00000: coded_6b (rd_in) ? 6b100111 : 6b011000; 6b0_00001: coded_6b (rd_in) ? 6b011101 : 6b100010; 6b0_00010: coded_6b (rd_in) ? 6b101101 : 6b010010; // ... 中间省略 6b0_11111: coded_6b (rd_in) ? 6b101011 : 6b010100; // K码的5b部分 6b1_11100: coded_6b (rd_in) ? 6b001111 : 6b110000; // K28 default: coded_6b 6b000000; endcase end注意这里的rd_in是编码前的当前RD而不是编码完成后的状态。在组合逻辑里用rd_in查表然后在时序逻辑里根据coded_6b的极性更新rd_mid中间RD再把这个rd_mid送给3b/4b编码器。这一步顺序要是搞反了整个码流就全乱了。K码的5b部分一定要用独立分支因为同一个输入D280x1C在作为D码和K码时是完全不同的编码。比如5b11100作为D码输出是01010这个序列的一部分但作为K28时输出是001111或110000两者差别巨大。下面这段是3b/4b编码和RD更新// 3b/4b encoder reg [3:0] coded_4b; always (*) begin casez ({k_in, data_in[7:5]}) 4b0_000: coded_4b (rd_mid) ? 6b1011 : 6b0100; // D.x.0 4b0_001: coded_4b (rd_mid) ? 6b1001 : 6b1001; // D.x.1 4b0_010: coded_4b (rd_mid) ? 6b0101 : 6b0101; // D.x.2 // ... 4b1_010: coded_4b (rd_mid) ? 6b0101 : 6b1010; // K.x.2 4b1_111: coded_4b (rd_mid) ? 6b1110 : 6b0001; // K.x.7 default: coded_4b 4b0000; endcase end // RD update always (posedge clk or negedge rst_n) begin if (!rst_n) begin rd_out 1b0; // 复位后约定RD为负也就是rd_in0表示RD- end else begin case ({coded_6b, coded_4b}) 12b000111_????: rd_out 1b1; // 6b不平衡取反极性 12b111000_????: rd_out 1b1; 12b????_0011: rd_out 1b1; // 4b部分导致的不平衡 12b????_1100: rd_out 1b1; default: rd_out rd_in; endcase end end最后组合起来data_out {coded_4b, coded_6b}位序是高位在前低6位放5b/6b的结果高4位放3b/4b的结果。这个位序是PCIE、千兆以太网等常用协议的标准做法写代码前务必确认你的协议文档里的bit顺序定义否则收发两端对不上会非常痛苦。3.3 编码器的时序与复位策略编码器全是纯组合逻辑加一级寄存器输出所以时序收敛很容易。真正要小心的是复位策略和RD初始状态。复位后RD必须明确约定为RD-就是rd_in0两端一致发送端和接收端如果复位不完全同步在系统刚上电的那段时间RD状态可能短暂不一致但这不影响后续稳定因为8b10b的RD自我纠错能力很强只要一个平衡码过去RD就重新对齐了还有一个小技巧如果你要在FPGA里做多通道并行编码比如4通道PCIE每个通道的RD状态必须是独立维护的不能用同一个全局RD。因为通道间初始相位可能不同共用一个RD会让对端解码彻底错乱。这一点从协议文档里看不出来但做过多通道集成的人都知道。4. 解码器与时钟恢复对齐工程真正复杂的部分4.1 解码器输入与对齐逻辑解码器的输入是连续的10bit码字流但接收端如何知道哪里是一个码字的边界答案是在数据流里搜索comma模式。物理层通常是先把串行数据一个bit一个bit地收进来然后在bit流里找K28.5comma的特征序列“0011111”或“1100000”找到后就把这个位置当作字节边界后续数据都按10bit为粒度切分。这个逻辑用Verilog实现时一般有两种方案滑动移位寄存器把收到的bit不断移入一个10bit寄存器每bit都检查是否匹配comma模式匹配后输出一个对齐脉冲同时产生load信号给后续的10bit打拍逻辑状态机方案维护一个bit计数器如果当前窗内不匹配comma则滑动1bit继续找直到找到为止方案1比较直观代码就是移位寄存器加一个比较器做高速率高带宽时可能资源稍多方案2节省逻辑但状态机控制要小心。我个人建议在GTP/GTX这类高速收发器里直接用IP核内部的comma检测但在教学或者在低速自定义链路上手写对齐逻辑能让你彻底理解底层原理。对齐完成之后数据就以10bit为单位进入解码主体。解码主体做的事刚好和编码器相反根据码字查表找到对应的8bit数据和K码使能同时用上一个码字的RD状态来验证当前码字的极性是否符合预期并更新RD。4.2 解码器源码核心片段解析解码器的查表和编码器几乎对称唯一的区别是解码器的输入是10bit码字需要同时匹配RD和RD-两种情况。always (*) begin casez (code_in) 10b011000_0100: begin data_out 8h00; k_out 1b0; end 10b100111_1011: begin data_out 8h00; k_out 1b0; end 10b100010_0100: begin data_out 8h01; k_out 1b0; end 10b011101_1011: begin data_out 8h01; k_out 1b0; end // ... 10b110000_0101: begin data_out 8hBC; k_out 1b1; end 10b001111_1010: begin data_out 8hBC; k_out 1b1; end default: begin data_out 8h00; k_out 1b0; err_out 1b1; // 无效码字 end endcase end注意default分支里的err_out信号这是解码器里非常有价值的附属产出。通过统计err_out的脉冲个数你能直接判断链路质量。如果误码率偏高err_out会持续有脉冲如果只是偶尔一个可能只是噪声干扰。在调试系统时用这个信号配合逻辑分析仪比如ILA观察远比盯着波形数bit高效。解码器的RD校验逻辑和编码器类似也必须两级更新。这里有一个常见误区只校验码字是否在合法编码表内而忽略了RD极性。有些非法场景下码字本身在表里存在但极性顺序不对比如连续出现两个RD极性的码字这属于非法码字序列单纯查表是发现不了的。工业级实现通常会同时检查“码字合法”和“RD跳变合法”两个条件。4.3 对齐后数据的字节序与位宽处理真实工程里很少做8bit的编解码接口因为串行链路的吞吐量太大了。比如千兆以太网是1.25Gbps的线速率如果数据通路只有8bit宽那需要250MHz的主频才能吞吐FPGA上时序压力很大。常用的做法是内部数据通路做32bit甚至64bit宽一个周期处理4个或8个字节。位宽扩展后对齐逻辑也要跟着变。如果你一次收到32bit的数据comma可能出现在其中任意一个字节位置所以搜索逻辑要同时检查4个位置并且根据找到的位置决定输出数据怎么“挪”到对齐后的格式上。看起来复杂但原理就是一个桶形移位器配合mux选通。Xilinx和Altera的收发器IP核在RX端都自带这个功能用FPGA开发时大部分情况可以直接用IP不用自己写但你要明白原理否则调IP参数时面对一堆选项会很懵。5. 仿真验证与上板调试从“看起来对”到“跑得稳”5.1 Testbench怎么写才最有效8b10b模块的testbench最理想的方式是回环测试发送端先把数据编码再把编码结果直接送给解码器然后比对解码后的数据和原始数据是否一致。这个回环测试能在仿真阶段把编解码器的大部分逻辑错误暴露出来。回环测试里要特别注意几个测试向量的选定遍历所有256个D码每个D码分别在RD-和RD两种状态下编码确保查表全覆盖遍历常用K码加随机数据混合验证K码使能通道构造连续的平衡码输入比如D21.5测试RD不翻转时模块是否稳定构造连续的不平衡码输入比如D0.0测试RD翻转是否按预期交替// 遍历所有D码的测试片段 integer i; initial begin for (i 0; i 256; i i 1) begin data_in i[7:0]; k_in 1b0; #10; // 检查解码输出是否等于data_in if (dec_data_out ! i[7:0]) begin $display(ERROR at data%02x, i); end end // 测试结束 $finish; endVivado自带的XSim或者ModelSim/QuestaSim都支持跑这个脚本。重点是波形覆盖率测试一定要确保遍历完所有RD状态否则某些输入在特定RD极性下的映射是查不到错误路径的。5.2 上板调试实战常见问题清单调试经验告诉我们这类编解码模块上板后80%的问题不是出在逻辑功能上而是出在接口时序和复位顺序上。下面这几个故障我在项目里至少各踩过一次故障现象根本原因解决方案接收端始终无法对齐发送端K码没发出来或者comma被其他逻辑修改先用ILA观测发送端发出的原始bit流确认K28.5序列正确能对齐但数据全是错的解码器表和编码器表的bit顺序不一致检查MSB/LSB定义输出10bit是否高低位搞反偶发误码跑一段时间才出错复位后两端RD初始状态不一致统一复位策略确认收发的RD初始化完全相同速率上去以后时序不过编码表用组合逻辑堆太大路径延迟超标拆分两级编码中间加流水寄存器第二个故障特别有意思。之前做一个自定义高速链路编码器输出的码字是按照协议文档的bit序高4位在前低6位在后但解码器表里我是按MATLAB脚本自动生成的脚本里没注意位序把高4位和低6位写反了。仿真回环时因为回环路径同一个模块编码端和解码端的查表都“一致地”错了所以仿真全通过——上板后收发两端一对接全部乱码。后来我在testbench里故意加入了一个“反序注入”的测试用例才把这个坑堵上。还有一个调试技巧如果链路能长时间跑通但偶尔误码建议在发送端周期性插入K28.5这样接收端可以定期做对齐复核。一旦数据错位K28.5能让对齐逻辑在几个字节内快速恢复。这也是PCIE里SKP字符存在的部分原因。5.3 结合IGT收发器GTP/GTX的联调注意事项如果你是用FPGA的GTP/GTX高速收发器来做物理层8b10b编解码一般可以直接使用收发器内部的编码器用配置选项开启。这样做的性能当然比自己写的Verilog模块要好毕竟是硬核实现。但有时候你还是需要自己写编解码比如使用非标准的编码变种、需要对编码过程做插桩观测级调试、或者收发器的编码器不支持你需要的K码组合。自写编解码模块接入收发器时最需要注意的是时钟域和位宽的匹配。收发器的内部接口FPGA侧接口通常已经是并行数据加字节时钟你拿到的是已经对齐好的并行数据编码模块工作在这个字节时钟域。所以你的编码器主时钟直接取字节时钟即可但要对做CDC跨时钟域的地方保持警觉。收发器复位时序和编码器复位时序如果要联动最好用同一次复位。6. 项目复盘我踩过的坑和给新手的建议做这个8b10b项目前后历时不到两周但收获比预想中大很多。复盘下来有几个点值得分享第一文档协议一定要自己推导一遍。8b10b的编码表网上到处都是但如果你直接抄表然后一头扎进代码你会发现自己根本不理解为什么某些码字有等价替代项为什么某些码字要强制跳过。给自己半天时间把编码表的生成逻辑理一遍比多改十次代码都强。我最后用的是从标准里抄的编码表但每一步都有手写推导的备查笔记。第二仿真用例要“故意做错”。一个高效的testbench不仅要测正确路径还要故意注入非法码字、反序码字、错误的RD极性看看异常路径有没有被检测到。你可以在解码器里加一个错误注入端口用testbench把某个码字强制翻转几bit然后检查err_out响应。这个特性在联调时特别有用能帮你快速给出“链路误码来自物理层还是协议层”的判断依据。第三做高速串行项目第一版就该考虑FPGA收发器的IP使用方式。自写8b10b作为学习完全值得但如果商用项目时间紧直接用收发器内建的8b10b编码器把精力放在上层的链路调试上会更高效。不是说自写没用而是要有意识地区分“学习目的”和“工程交付”两种模式。第四代码要有记录波形导出的习惯。如果你想分析一个跑了很久的偶发误码不要只靠仿真。在FPGA里把收发器的数据、RD状态、err信号接到ILA上触发条件设为err1等误码发生的那一瞬抓下波形基本一眼就能看出问题。没有抓住现场波形之前不要轻易猜原因——串行链路的问题经常和你最初怀疑的地方完全无关。最后再分享一个小技巧如果你在做多通道的编解码把各个通道的RD状态做成可以软件读取的寄存器上板调试时通过串口或以太网远程观测每个通道的RD跳变频率。如果某个通道的RD跳变明显异常那这个通道的物理链路大概率有质量问题。这个方法看着土但在我做8通道JESD204B的时候帮我在十分钟内定位到了一个PCB布线引起的通道间串扰问题。本文还有配套的精品资源点击获取
返回列表