
1. 为什么高速串行链路非要先做8B/10B编码我第一次认真翻 8B/10B 编码表是因为一块 FPGA 板子上的 SATA 链路怎么都建不起来。误码率仪上显示码流本身没丢东西但接收端的字对齐状态机就是在反复重试。后来把示波器接上去看眼图才发现编码器输出的码流里有大段相同电平跳变稀疏到时钟数据恢复电路几乎抓不住相位。那一刻我才真正理解8B/10B 编码从来不是把 8 位变成 10 位这么简单的一个映射表它是整个高速串行链路物理层能不能站住脚的地基。这套编码最早由 IBM 在 1983 年提出后来被写进大量串行协议标准里。它的核心任务就三件事把数据流改造成直流平衡的形态让交流耦合链路不产生基线漂移保证足够的电平跳变密度让接收端的时钟数据恢复电路有边沿可用再顺带提供一层有限的错误检测和快速字对齐能力。适合读这篇文章的大概是这几类人正在调 SerDes 或者高速接口链路的硬件工程师写 RTL 需要自己搭编码器或者解码器的 FPGA 开发者做协议一致性测试的验证人员以及被面试问到为什么需要线路编码时想给出有分量回答的人。下面我按自己实际做项目的顺序把这套编码从动机到实现再到踩坑完整梳理一遍。1.1 交流耦合链路里的直流平衡难题现在的高速串行链路发送端和接收端之间几乎都串了隔直电容容量通常在 100nF 量级。这个电容的作用是隔离两端的共模电压差异让链路可以跨不同的供电域和工艺节点工作。但它同时也是一道高通滤波器任何接近直流的低频分量都会被衰减掉。问题就出在这儿如果原始数据里长时间出现 1 比 0 多、或者 0 比 1 多的情况码流的平均值就会偏离判决门限接收端的判决点会随着时间缓慢漂移这就是常说的基线漂移。漂移幅度一旦超过眼图裕量采样就会出错。8B/10B 的做法很直接它保证任意一段足够长的码流里1 和 0 的数量差被严格限制在一个很小的范围内。具体到单个 10 位码字1 和 0 的个数差不会超过 2跨码字累积起来这个偏差也不会被放大。这样一来码流的直流分量始终贴在判决门限附近电容不会把信号吃掉。这一条是硬性约束不是优化目标所以任何一次编码器查表出错最先暴露出来的往往就是接收端的极性错误计数。1.2 时钟数据恢复对跳变密度的硬性要求高速串行链路通常不单独传时钟接收端靠码流本身的电平跳变来恢复时钟这套机制叫时钟数据恢复也就是 CDR。它的原理是让一个本地压控振荡器或者相位插值器去跟踪输入码流的边沿用边沿做相位比较再用环路滤波器把相位误差压掉。这套机制有个前提边沿得够密。如果码流里连续几十个比特都是同一个电平边沿就消失了CDR 的相位比较器没有输入环路只能靠自身的频率精度自由运行时间一长相位就飘走再回到有跳变的位置时已经错过了采样窗口。8B/10B 给了一个确定性的保证编码后的码流中任意 5 个连续相同电平之后必然出现一次跳变也就是最大游程长度为 5。这个数字不是随便定的它是 CDR 环路带宽和相位误差累积之间折中的结果。游程再长一点低带宽的 CDR 可能真的锁不住限制得再短一点编码效率会进一步下降。相比之下如果只用加扰器跳变密度只能在统计意义上保证遇到特定的输入序列仍然可能出现长游程。8B/10B 是确定性的这是它最大的价值。1.3 顺带拿到的链路监测能力除了直流平衡和跳变密度这两条硬指标8B/10B 还提供了两个额外的好处在实际调试里非常好用。第一个是错误检测。10 位码字空间一共 1024 种组合标准里实际用掉的只有两百多个。接收端拿到一个 10 位码字先查表看它是不是合法码字同时还要检查它的极性和当前累积的不均衡状态是否一致。这两种检查加起来能发现相当一部分传输错误。当然它检不出全部单比特错误因为有些合法码字之间只差一个比特但作为一个免费的链路健康指示已经很有价值了。第二个是快速字对齐。编码后的码流里存在一些特殊的控制字符它们的比特模式在正常的跨字符边界上不可能出现。接收端只要在比特流里滑动搜索这种模式就能在最多几十个比特的时间内确定 10 位字符的边界。这个能力对链路建立的响应速度影响很大后面讲 K 码的时候我会展开说。2. 编码架构拆解为什么拆成5B/6B加3B/4B很多人第一次看到 8B/10B 的编码表会疑惑为什么不是一张 256 项的 8 位到 10 位映射表而要拆成低 5 位查 6 位、高 3 位查 4 位两张表。这个设计选择背后有很实际的工程考量理解了它后面算极性、算游程都会顺很多。最直接的原因是表的规模。如果直接做 8 位到 10 位的映射每个输入字节要对应 RD- 和 RD 两种输出就是 512 项而且每一项都得人工验证游程和极性约束出错概率很高。拆成 53 之后低 5 位只需要 32 项高 3 位只需要 8 项两张表加起来 40 项核查工作量小了一个数量级。更深一层的原因是极性控制的粒度。把码字拆成 6 位和 4 位两个子块之后每个子块自身的不均衡度都不超过 2两个子块串起来整个 10 位码字的不均衡度也就被限制在 2 以内。而且在编码过程中6 位子块编完之后可以立刻更新一次累积不均衡状态再用更新后的状态去查 4 位表这样极性控制是分段完成的逻辑非常清晰。2.1 8位数据怎么拆Dx.y命名法怎么读标准定义的拆分方式是固定的一个字节 bit7 到 bit0高 3 位bit7、bit6、bit5记作 H、G、F低 5 位bit4 到 bit0记作 E、D、C、B、A。低 5 位先编码成 6 位记作 a、b、c、d、e、i高 3 位再编码成 4 位记作 f、g、h、j。最后拼接的顺序是 a b c d e i f g h j注意 i 夹在中间这一点很容易在实现的时候搞错位序。命名法用 Dx.y 的形式其中 x 是高 3 位的十进制值范围 0 到 7y 是低 5 位的十进制值范围 0 到 31。控制字符用 Kx.y 表示同样结构。举个例子字节 0x59 展开成二进制是 0101 1001。高 3 位是 010十进制 2低 5 位是 11001十进制 25。所以这个字节的编码标识就是 D25.2。再比如 0x00 就是 D0.00xFF 是 D31.70xBC 是 D28.5。这个命名法在协议手册和 IP 文档里到处都是能快速口算出来会省很多翻表的时间。字节值高3位 HGF低5位 EDCBA编码标识0x0000000000D0.00x5901011001D25.20xB510110101D21.50xBC10111100D28.50xFF11111111D31.72.2 5B/6B映射表与码字挑选的约束条件低 5 位到 6 位这一级输入空间是 32 种输出空间是 64 种也就是要从 64 个 6 位组合里挑出 32 个来用再把剩下的做极性配对。挑选过程受几条约束同时限制。第一条是游程约束。6 位子块的内部游程不能超过 5再加上和前后子块拼接时的边界情况最终要保证整条码流的最大游程是 5。所以像 000000 和 111111 这种组合直接被排除因为它们内部就是 6 个相同电平。第二条是不均衡度约束。每个 6 位码字里 1 和 0 的数量差最大只能到 2。像 111110 这种有 5 个 1、1 个 0 的组合差值是 4直接被排除。这也就把 6 位空间里 disparity 超过 2 的组合全部筛掉了。第三条是互补配对。对于不均衡度不为 0 的码字标准要求它的按位取反也必须被使用这样 RD- 和 RD 两种情况下都能找到合适的码字。比如 D0.0 在 RD- 时编码成 100111不均衡度是 2在 RD 时编码成 011000不均衡度是 -2两者正好互为按位取反。而对于不均衡度为 0 的中性码字RD- 和 RD 时使用同一个码字比如 D25.0 在两种状态下都编码成 100110。输入 EDCBA标识RD- 输出 abcdeiRD 输出 abcdei不均衡度00000D0.01001110110002 / -200011D3.0110001110001000101D5.0101001101001001111D15.00101111010002 / -211001D25.010011010011002.3 Running Disparity编码器里唯一的状态量Running Disparity 这个词翻译过来叫运行不一致性是整个 8B/10B 编码器里唯一需要保存的状态。它记录的是从链路开始到当前时刻已经发送的码流里 1 比 0 多还是少。约定是这样的如果到目前发送的码流里 1 的个数多于 0记作 RD反过来记作 RD-。编码时有一条基本规则当前是 RD-就优先选择不均衡度为正或者为零的码字把累积偏差往正方向拉当前是 RD就选择不均衡度为负或者为零的码字。这样累积偏差永远在正负之间小幅摆动不会单向漂移。编码流程上一个字节的处理顺序是这样的先拿当前的 RD 去查低 5 位的 6B 表得到 6 位输出然后根据这 6 位里 1 的个数判断它的不均衡度如果 1 比 0 多就翻转 RD1 比 0 少也翻转相等就保持再用更新后的 RD 去查高 3 位的 4B 表得到 4 位输出最后再更新一次 RD作为下一个字节的入口状态。这里有个容易搞错的地方RD 是在 6 位子块和 4 位子块之间就更新一次的不是等整个 10 位拼完才更新。如果实现的时候把这一步省掉用入口 RD 同时查两张表输出的码字极性就会出现系统性偏差在长码流上会累积成明显的直流偏移。还有一个细节是初始 RD。链路刚建立的时候编码器需要一个确定的起始状态标准规定从 RD- 开始。发送端和接收端必须约定同一个初始值否则接收端的极性检查会从头就是错的。这个初始值在大部分协议里是硬性规定的不需要额外配置。3. 完整走一遍编码流程从字节到10位码字光看表和公式容易飘我拿几个真实的字节完整走一遍把所有中间状态都列出来。你跟着算一遍整个机制就落地了。这一节我假设链路刚建立入口 RD 为 RD-。3.1 单个字节的完整编码过程先拿最简单的 0x00 也就是 D0.0 开刀。低 5 位 EDCBA 全是 0查 6B 表RD- 对应输出 100111。数一下 1 的个数有 4 个0 有 2 个不均衡度是 2所以 RD 翻转从 RD- 变成 RD。接下来高 3 位 HGF 全是 0查 4B 表D.x.0 在 RD 时输出 0100。这 4 位里 1 有 1 个0 有 3 个不均衡度是 -2RD 再翻转从 RD 变回 RD-。最后拼接顺序是 abcdei 加 fghj得到 1001110100。整个码字里 1 有 5 个0 有 5 个总不均衡度为 0出口 RD 是 RD-和入口一致。再试一个中性的例子0x59 也就是 D25.2。低 5 位是 11001查 6B 表D25.0 在两种 RD 下都输出 1001101 和 0 各 3 个不均衡度为 0RD 保持 RD- 不变。高 3 位是 010对应 D.x.2RD- 时 4B 输出 0101。这 4 位里 1 有 2 个0 有 2 个不均衡度 0RD 继续保持 RD-。拼接得到 1001100101整个码字里 1 有 5 个0 有 5 个出口 RD 还是 RD-。3.2 连续码流的RD演化与极性检查单个字节看不出问题连续几个字节才能看出 RD 是怎么起作用的。我把前面三个字节串起来每个字节都以 RD- 为入口看看累积效果。步骤当前字节入口 RD6B 输出6B 后 RD4B 输出出口 RD10 位码字10x00 (D0.0)RD-100111 (2)RD0100 (-2)RD-100111010020x59 (D25.2)RD-100110 (0)RD-0101 (0)RD-100110010130x0F (D15.0)RD-010111 (2)RD0100 (-2)RD-0101110100三行数据有个共同特点出口 RD 都回到了 RD-。这不是巧合而是 8B/10B 的设计目标之一。当一个字节的两级子块不均衡度符号相反时整个码字的不均衡度就是 0RD 净变化为零链路状态保持稳定。只有出现净不均衡度不为零的码字时RD 才会真正翻转并带到下一个字节。再看一个会真正翻转的例子。假设当前 RD-编码 D18.6也就是字节 0xD2。低 5 位是 10010查表得 0100111 有 3 个、0 有 3 个不均衡度 0RD 保持 RD-。高 3 位是 110对应 D.x.6RD- 时输出 01101 有 2 个、0 有 2 个不均衡度 0RD 还是 RD-。整个码字 0100110110净不均衡度 0。真正让 RD 翻转的是像 D15.0 那样的码字6B 部分 24B 部分如果 RD 时选了 0100 就是 -2一正一负抵消。但假如选的是 1011那就会一路正下去。这就是为什么编码表必须和 RD 状态严格绑定不能随便挑。提示接收端做极性检查的时候用的是和发送端同样的规则。如果收到的码字在当前 RD 下不应该出现就会报一个极性错误。这类错误计数在协议层是可以读出来的调试链路的时候非常有用。3.3 游程长度是怎么被压住的游程长度是很多人忽略的一个维度但它直接决定了 CDR 能不能工作。8B/10B 保证任意连续 5 个相同电平之后必然有一次跳变这个结论是靠两层的约束共同保证的。第一层是子块内部。6 位子块和 4 位子块在挑选时都排除了全 0 和全 1也排除了内部游程超过限制的组合所以单个子块内部的游程不会超标。第二层是子块之间的边界。这里有个不那么直观的地方D.x.7 这个特殊的输入标准为它准备了两种 4B 编码。主编码是 1110 和 0001备用编码是 0111 和 1000。什么时候用备用编码取决于前面 6 位子块末尾的比特模式。这样做的目的就是避免 6 位子块的尾部连续的 1 和 4 位子块开头的 111 拼在一起形成超过限制的长游程。举个具体的例子D21.5 也就是 0xB5编码结果是 1010101010。这个码字里最长连续相同电平只有 1 位交替得非常均匀所以它经常被用在链路空闲序列里用来维持 CDR 锁定。相比之下K28.7 的编码是 0011111000里面有一段 5 个连续的 1正好卡在允许的最大游程上所以它常被用作同步字符。我实际调试的时候遇到过一次 CDR 失锁最后定位到是自定义的控制字符用错了编码产生了 6 个连续相同电平。链路在大部分时间能工作但每隔一段时间就丢一次同步。所以自定义 K 码的时候一定要把游程算一遍别只看它是不是合法码字。4. K码与有序集链路管理真正的抓手数据编码只是 8B/10B 的一半功能另一半是控制字符。标准在 256 个数据码字之外额外定义了 12 个控制字符用来承载链路管理信息。这 12 个字符在整个协议体系里的地位其实比数据编码更高因为链路能不能建立起来、能不能从异常中恢复全靠它们。4.1 K28.5与逗号序列字对齐是怎么实现的接收端面对的是连续不断的比特流它不知道哪 10 位是一个字符。如果每次都要靠协议层给边界链路建立会非常慢。8B/10B 的解决办法是在码流里嵌入一些特殊模式这些模式在正常的字符拼接处不可能出现接收端只要滑动搜索就能定位边界。最著名的就是 K28.5它的 10 位编码在 RD- 时是 0011111010在 RD 时是 1100000101。这两种编码里都包含一段 7 位的模式0011111 或者它的按位取反 1100000。这个 7 位模式就是所谓的逗号序列。为什么它不会在字符边界上误出现因为两个相邻的 10 位字符拼在一起的时候跨边界的 7 位窗口最多只会包含一个字符的尾部和一个字符的头部。而 8B/10B 的编码约束保证了任何合法的字符头部和尾部组合都不会拼出 0011111 或者 1100000 这七位。所以接收端一旦扫到这个模式就能确信自己找到了一个 K28.5 字符的起始位置字符边界也就确定了。实际实现上接收端通常用一个 7 位移位寄存器持续监测一旦匹配到逗号序列就把对齐计数器复位同时开始按 10 位一组切分后续的码流。这个过程通常需要连续几次匹配确认避免被传输错误导致的伪模式骗过去。链路建立阶段的对齐时间直接决定了协议握手的速度。控制字符RD- 编码RD 编码常见用途K28.000111110111100000100链路空闲、时钟容差序列K28.100111110011100000110帧起始、同步K28.500111110101100000101字对齐、链路同步K28.700111110001100000111同步、唤醒K23.711101010000001010111帧结束K27.711011010000010010111帧起始K29.710111010000100010111帧结束K30.701111010001000010111错误传播、链路异常4.2 常用有序集在协议里的分工实际协议里很少单独使用一个控制字符而是把控制字符和数据字符按固定顺序组合成有序集。有序集是对齐后的完整字符序列接收端可以整体识别不需要逐字符解析。最基础的是空闲序列它持续在链路空闲时发送作用是维持 CDR 锁定、维持字对齐、同时告诉对端我还在。不同协议的空闲序列内容不一样但结构类似通常是一个或者几个对齐字符加上若干数据字符。第二类是同步和训练序列。链路刚上电的时候双方的发送端参数、接收端均衡都还没配好需要先发一段固定模式让对端适应。这个序列里通常包含 K28.5 用于对齐再配合特定的数据字符组合让接收端评估信道质量。第三类是帧定界。帧起始和帧结束都需要明确的标记而且这个标记必须在数据流里不可能自然出现。K27.7 和 K23.7 就是干这个的它们各自有自己的编码特征接收端一识别到就知道一帧数据的边界在哪里。第四类是错误指示。当接收端检测到非法码字或者极性错误可以通过发回特定的错误字符让发送端知道链路出了问题。有序集类型典型组成主要作用空闲K28.5 D21.5 重复维持锁定与对齐同步K28.5 K28.5 或特定组合建立初始同步帧起始K27.7 若干数据字符标识帧头帧结束K23.7 或 K29.7标识帧尾错误K30.7 组合通知对端链路异常4.3 K码设计上的取舍标准在选择这 12 个控制字符的时候其实做了不少权衡。我在读协议文档的时候留意过几点挺有意思的。第一点是为什么集中在 K28.x 上。K28 这个位置的低 5 位是 11100它的 6B 编码是 001111 和 110000这两个模式天然包含逗号序列所以非常适合做对齐和同步。而 K23.7、K27.7、K29.7、K30.7 这几个低 5 位各不相同它们的 6B 编码也都是特定的模式彼此之间差别较大适合做那些需要明确区分、不容易混淆的标记比如帧定界。第二点是错误检测的考虑。K 码的码字之间保持了相对较大的差异这样接收端在判断一个码字是不是某个特定控制字符时抗干扰能力更强。如果两个控制字符的码字只差一两个比特传输中出现单比特错误就可能被误判成另一个控制字符那链路状态机就乱了。第三点是字节值的唯一性。有些控制字符和某些数据字符共享同一个 8 位值比如 K28.5 和数据字节 0xBC 的值是一样的。标准通过独立的编码表来区分它们编码器在同一个字节位置上选择发数据还是发控制是上层逻辑决定的。这也就意味着协议必须在结构上明确约定哪些位置发控制字符不能含糊。5. 用Verilog实现编解码器结构划分与关键代码弄清了原理接下来就是把它变成 RTL。我在几个项目里写过 8B/10B 的编码器和解码器结构其实很固定难的是把极性和位序这两件事处理干净。下面把我实际用的结构说一遍。5.1 编码器的模块划分与流水线设计编码器我一般拆成三个模块6B 编码查找、4B 编码查找、RD 状态更新。三级之间可以加一级流水线寄存器提高时序余量。6B 编码模块的输入是 5 位数据和当前 RD输出是 6 位码字和更新后的 RD。这个模块本质上就是一个 64 项的查找表用 RD 和输入数据拼成的 6 位地址去取结果。4B 编码模块的输入是 3 位数据和 6B 编码后的 RD输出是 4 位码字和最终 RD。同样是查找表16 项地址。RD 状态更新模块负责根据当前输出的不均衡度翻转或者保持 RD。不均衡度可以通过数 1 的个数得到6 位里 1 的个数大于 3 说明不均衡度为 2小于 3 说明是 -2等于 3 说明是 0。这三个模块串起来一个字节的编码延迟通常是 1 到 2 个时钟周期对于 10 Gbps 级别的线速率来说完全够用。如果用高位宽接口比如 32 位或者 64 位并行就直接实例化多个编码器然后做位拼接注意每个通道的 RD 是串联传递的。5.2 关键代码片段下面是我常用的编码器核心逻辑把两张表做成常量数组综合工具会自动推断成查找表面积和速度都不错。module enc8b10b ( input wire clk, input wire rst_n, input wire rd_in, // 1 RD, 0 RD- input wire is_k, // 1 控制字符 input wire [7:0] din, // {HGF, EDCBA} output reg [9:0] dout, // {abcdei, fghj} output reg rd_out ); // 5B/6B 查找表索引 {rd, EDCBA}共 64 项 reg [5:0] lut6 [0:63]; // 3B/4B 查找表索引 {rd, HGF}共 16 项 reg [3:0] lut4 [0:15]; wire [4:0] low5 din[4:0]; wire [2:0] high3 din[7:5]; reg [5:0] code6; reg [3:0] code4; reg rd_mid; reg rd_next; wire [2:0] ones6 code6[0] code6[1] code6[2] code6[3] code6[4] code6[5]; wire [1:0] ones4 code4[0] code4[1] code4[2] code4[3]; always (*) begin code6 lut6[{rd_in, low5}]; // 6 位子块不均衡度为 0 时保持 RD否则翻转 rd_mid (ones6 3) ? rd_in : ~rd_in; code4 lut4[{rd_mid, high3}]; rd_next (ones4 2) ? rd_mid : ~rd_mid; end always (posedge clk or negedge rst_n) begin if (!rst_n) begin dout 10b0; rd_out 1b0; // 复位后从 RD- 开始 end else begin dout {code6, code4}; rd_out rd_next; end end endmodule这段代码里有两个地方值得单独说。一个是位拼接的顺序。dout 是 {code6, code4}对应 a b c d e i f g h jcode6 在左、code4 在右。如果按照直觉写成 {code4, code6}输出就是完全错的接收端一个字符都对不上。我第一次写的时候就在这里栽过跟头仿真跑出来波形怎么看都不对后来把编码表的顺序对着协议文档核了一遍才发现。另一个是 RD 更新的时机。这里是在 6B 编码后先算出 rd_mid再用它去查 4B 表最后算出 rd_next。如果偷懒直接用 rd_in 查两张表短数据还看不出来长数据流上就会出现累积极性偏差。5.3 仿真验证的自检清单编解码器写完之后验证这一步千万别省。我一般会做几组强制检查用脚本自动跑跑通了才敢上板。第一组是全遍历测试。把 0 到 255 所有字节都编一遍每一个都检查三件事输出的 10 位码字里1 和 0 的个数差不超过 2最大连续相同电平不超过 5把码字再送回解码器能还原出原始字节。这组测试能覆盖绝大部分表项错误。第二组是连续码流测试。用伪随机序列生成几百万个字节持续编码然后统计整段码流里 1 和 0 的总数差。如果编码器极性处理正确这个差值应该始终在一个很小的范围内波动不会单向增长。我一般在脚本里设一个阈值比如差值绝对值超过 16 就报警。第三组是控制字符测试。把 12 个 K 字符都编一遍逐个核对编码表。同时检查包含 K28.5 的有序集确认逗号序列的位置正确接收端能对齐。第四组是错误注入测试。在仿真里人为翻转码流中的某个比特看解码器能不能检出非法码字或者极性错误。这里要有心理准备不是所有单比特错误都能检出能覆盖大部分就说明实现是对的。检查项方法判定标准码字不均衡度统计每个 10 位码字的 1 个数与 5 的差不超过 2最大游程逐位扫描连续相同电平不超过 5编解码回环编码后立即解码与原始字节一致累积极性长随机序列统计总差值在阈值内波动K 码正确性逐个比对编码表与标准一致错误检出注入单比特错误多数能被检出6. 工程实践中踩过的坑与排查思路理论讲完说点实际的。8B/10B 相关的问题表现形式五花八门但根因基本就那么几类。我把这些年遇到的整理一下列成表方便你对照。6.1 典型故障速查表现象可能原因排查方向链路完全建不起来初始 RD 不一致、K 码用错检查两端初始状态和有序集定义偶发丢同步码流中出现