简介:面向高速串行通信、FPGA与SerDes方向的学习者,这份PDF围绕8B/10B编码展开系统讲解,从DC平衡这一核心诉求切入,梳理其为何要把8位数据映射为10位传输。文档重点覆盖5B/6B与3B/4B两级编码机制、Dx.y与Kx.y表示法、Disparity与Running Disparity概念,并说明D.x.P7、D.x.A7候选编码如何避免连续5个0或1。对K.28.1、K.28.5、K.28.7等逗号序列及控制代码的校准作用也有具体说明,同时提及USB3.0、PCI Express、Serial ATA、Fiber Channel等典型应用与错误检测恢复思路。包内共1个PDF文件,约560KB,轻量单文档,便于按知识点检索与复习。已有306人学习下载,适合需要理解编码表、掌握直流平衡与链路校准机制的中初级读者查漏补缺。
1. 8B/10B 编码为什么存在:DC 平衡与高速串行链路的底层约束
把 8 位并行数据直接推到差分对上,还是先编码成 10 位再发?1983 年 IBM 为 ESCON 选后者时,代价不只是那 25% 的带宽开销。真正的痛点在接收端的 AC 耦合电容上:连续长串的 1 或 0 会让电容两端充放电失衡,判决门限跟着漂移,眼图直接塌陷。这套由 Al Widmer 和 Peter Franaszek 在 IBM 研究与开发刊物上描述的编码,用每 5 个连续相同位强制插入一位反向位的方式,把最长游程锁死在 5 以内,同时保证每个 10 位码字的 0/1 数量只可能是 5:5、6:4 或 4:6。
这就是 DC 平衡。今天 USB 3.0、SATA、PCI Express、InfiniBand、Fiber Channel、RapidIO、1394b 的物理层里都还留着它的影子,早期用曼彻斯特编码或 4B/5B 的场景,要么效率更低要么游程控制不如它彻底。这份资料适合两类人:正在做 SerDes 或协议层解码的固件工程师,以及需要读懂链路误码日志、判断到底是编码问题还是信道问题的调试人员。
2. 从 Dx.y 到 10bit 码字:5B/6B 与 3B/4B 的拆分逻辑
2.1 位序约定:HGF EDCBA 与 abcdei fghj 的映射
8B/10B 最容易翻车的地方不是算法本身,而是位序。一个字节按 HGFEDCBA 排列,H 是最高位、A 是最低位;传输时拆成两段——低 5 位 EDCBA 走 5B/6B,高 3 位 HGF 走 3B/4B。编码后的 6 位记作 abcdei,4 位记作 fghj,上线的顺序是 abcdeifghj。
数据值统一写成 Dx.y 或 Kx.y,D 表示数据码、K 表示控制码,x 是 EDCBA 的十进制值(0~31),y 是 HGF 的十进制值(0~7)。原文举的那个例子很直观:字节 10110101 从高到低拆开,HGF = 101 = 5,EDCBA = 10101 = 21,所以这个字节就是 D21.5。用 Dx.y 记录,比反复写二进制串要好核对得多,尤其在查表和写回归用例的时候。
| 记号 | 位宽 | 原始位 | 编码后 | 说明 |
|---|---|---|---|---|
| x | 5 bit | EDCBA | abcdei | 低 5 位,决定 5B/6B 查表索引 |
| y | 3 bit | HGF | fghj | 高 3 位,决定 3B/4B 查表索引 |
| Dx.y | 8 bit | HGFEDCBA | abcdeifghj | 普通数据码字 |
| Kx.y | 8 bit | HGFEDCBA | abcdeifghj | 控制码,用于对齐和原语 |
2.2 5B/6B 编码表的结构与 D.x.7 的候补码
5B/6B 表把 32 个 5 位输入映射到 6 位输出,其中一部分码字是完美平衡的(3 个 1、3 个 0),无论当前 Running Disparity 是正是负都用同一个值;另一部分是不平衡的,必须准备一对互补码字,按当前 RD 二选一。判断一个码字该不该翻转,靠的是它的 disparity——1 的个数减 0 的个数。
真正绕人的是 D.x.7 这一行。当 5B/6B 的输出本身已经偏向某一侧,再拼上 3B/4B 的 4 位,很容易凑出连续 5 个 0 或 1,破坏游程约束。所以标准给 y=7 预留了两个变体:D.x.P7 和 D.x.A7,编码器必须从中挑一个避开违例。资料里给出的规则是,D.x.A7 只在 x=17、18、20 且 RD=-1 时使用,或者在 x=11、13、14 且 RD=+1 时使用;当 x 落在 23、27、29、30 这几个值时,则改用 K.x.7 编码。除此之外的位置强行用 A7 会和其他逗号序列撞车。
先写个小函数把字节拆明白,后面查表都靠它:
def split_byte(b): """把一个 8bit 字节拆成 x(EDCBA, 低5位) 与 y(HGF, 高3位)""" x = b & 0x1F # 取低 5 位,对应 EDCBA y = (b >> 5) & 0x07 # 取高 3 位,对应 HGF return x, y for v in (0xB5, 0xBC, 0x50): x, y = split_byte(v) print(f"0x{v:02X} -> D{x}.{y}")b & 0x1F用掩码取低 5 位,等价于对 32 取模;(b >> 5) & 0x07先右移 5 位再掩 3 位,得到高 3 位。0xB5 输出 D21.5,0xBC 输出 D28.5,0x50 输出 D16.2。这个映射在编解码两端必须完全一致,反了就会出现「数据能通但 K 码全错」的诡异现象。
2.3 3B/4B 编码与 K.x.7 的替换时机
3B/4B 这一侧只有 8 个输入,表比 5B/6B 短得多,但它的坑在于和控制码共用索引空间。K.x.7 之所以要被单独拿出来替换掉 D.x.7,是因为逗号序列只在 y=7 这一列才会产生——K28.7 对应的 4 位输出能拼出 0011111 或 1100000 这种 5 连位模式,接收端正是靠它来定位 10 位边界。
提示:D.x.7 的 P7/A7 选择依赖当前 RD,而 K.x.7 的选择还额外依赖「当前码字是不是控制码」。把这两件事写在同一个 if 分支里,是初学者最常见的错误来源。
3. Running Disparity 与逗号序列:K 码和链路对齐
3.1 Disparity 与 Running Disparity 的递推关系
Disparity 是单个 10 位码字的极性,取值只有 -2、0、+2 三种;Running Disparity(RD)是这条链路上截至当前码字的累计状态,初始化为 -1,每次发送或接收一个码字就更新一次。更新规则很干脆:如果新码字的 disparity 大于 0,RD 置为 +1;小于 0 置为 -1;等于 0 则保持不变。资料里说的 +1 表示 1 比 0 多、-1 表示 0 比 1 多,讲的就是这个状态量。
为什么必须递推而不是逐码字独立判断?因为 DC 平衡是整条流的性质,不是单个码字的性质。编码器看到一个正值 RD,就会优先挑一个带负 disparity 的码字把账抹平;接收端用同一套规则重算 RD,一旦发现收到的码字在当前 RD 下不合法,立刻就能判定链路出错。这种「用状态机做校验」的思路,比单纯加 CRC 更早发现问题。
def disparity(code10): """统计 10bit 码字的极性,1 的个数减 0 的个数""" return code10.count('1') - code10.count('0') def next_rd(rd, code10): """根据当前码字更新 Running Disparity""" d = disparity(code10) if d > 0: return 1 if d < 0: return -1 return rd # 完美平衡码字不改变 RD 极性 k285_neg = '0011111010' # 当前 RD 为负时发送的 K28.5 k285_pos = '1100000101' # 当前 RD 为正时发送的 K28.5 print(disparity(k285_neg), next_rd(-1, k285_neg)) # -2, 变成 -1 print(disparity(k285_pos), next_rd( 1, k285_pos)) # 2, 变成 +1注意这里next_rd的返回值和输入 RD 无关,只由码字自己决定极性,这是因为 8B/10B 的合法码字本身已经设计成「要么平衡,要么能明确指向某一侧」。接收端反过来的用法是:先算出码字的 disparity,再检查它是否和当前 RD 冲突,冲突就说明码字非法。
3.2 12 个控制字符与 K28.x 逗号序列
标准里定义了 12 个控制字符,它们在数据流里表示的不是业务数据,而是同步、对齐、空闲、错误传播这类链路层语义。其中 K28.1、K28.5、K28.7 是真正意义上的逗号序列——它们的 10 位码字里含有 0011111 或者 1100000 这样的 5 连位模式,而这种模式在任何 D 码字里都不会出现。接收端只要扫到这个特征串,就能把 10 位边界锁死,进而完成字对齐。
| 控制码 | RD- 时 10bit | RD+ 时 10bit | 典型用途 |
|---|---|---|---|
| K28.1 | 0011111001 | 1100000110 | 同步原语组成部分 |
| K28.5 | 0011111010 | 1100000101 | 逗号序列,字对齐 |
| K28.7 | 0011110111 | 1100001000 | 逗号序列,用于环回等场景 |
| K27.7 | 1101101000 | 0010010111 | 对齐与填充 |
注意:K28.5 是 PCIe、SATA、Fiber Channel 里出现频率最高的控制码之一,很多链路层协议直接用它做 COM 字符。抓包时如果看到连续的 K28.5 交替出现,通常说明链路处于空闲或复位状态,而不是数据错误。
3.3 多个 K28.7 同时出现为什么会让对齐失效
资料里有一句容易被忽略的话:多个 K28.7 序列不允许被同时使用,否则会产生不可探测的逗号序列。道理并不复杂——字对齐算法依赖「特征串在一段窗口内唯一出现」这个前提。如果一段流里塞进多个 K28.7,扫描窗口里可能同时命中多个候选位置,状态机无法判断哪一个是真正的 10 位边界,轻则对齐抖动,重则锁到错误的相位上,之后所有数据都是乱码。
实际工程里的做法是:K28.7 一般只在特定原语里出现一次,且前后必须有足够距离;接收端做对齐时,通常会要求连续 N 个候选位置都命中同一个相位,才正式切到锁定状态。这个 N 的取值在协议规范里写死了,但在自己写验证模型时可以调,调小了容易误锁,调大了锁定慢。
4. 用 Python 搭一套可复现的 8B/10B 编解码器
4.1 码表的数据结构设计
编码表有两种存法:按 8 位值索引的一维数组(长度 256,每个元素存 RD- 和 RD+ 两个 10 位码字),或者按 x、y 分开存的两张表。推荐后者,因为 D.x.7 的候补逻辑本身就是按 x 和 y 分开判断的,拆开写查表分支更清楚,也方便单测。
# 完整码表以 ANSI X3.230 / IEEE 802.3 规范为准,此处给出最小可运行骨架 FIVE_B_SIX_B = { # x: (RD- 的 6bit 码字, RD+ 的 6bit 码字) 0: ('100111', '011000'), 28: ('001111', '110000'), # K28.x 在 5B/6B 阶段共用同一对码字 } THREE_B_FOUR_B = { # y: (RD- 的 4bit 码字, RD+ 的 4bit 码字) 1: ('1001', '0110'), 5: ('1010', '0101'), }字典的 key 用 x 和 y 的十进制值,value 是二元组,第一项对应 RD 为负时该发的码字,第二项对应 RD 为正时该发的码字。查表前先算 RD,再按 RD 取下标,避免在每个分支里重复判断。
4.2 5B/6B 与 3B/4B 的查表与 RD 修正
编码主流程是四步:拆字节、查 5B/6B、决定 y=7 用不用候补码、拼 10 位并更新 RD。第三步是整个实现里唯一需要条件分支的地方。
def encode_byte(x, y, is_k, rd): six = FIVE_B_SIX_B[x][0 if rd < 0 else 1] four = THREE_B_FOUR_B[y][0 if rd < 0 else 1] code10 = six + four return code10, next_rd(rd, code10)rd < 0时取二元组第 0 项,否则取第 1 项。返回的第二个值是新 RD,调用方必须把它存下来传给下一次编码,这是串行链路编码器的核心状态。漏掉这一步,编出来的码字单独看都合法,拼起来却会出现连续 6 个以上的相同位。
提示:写单测时不要只校验单个码字,要构造一段 100 个以上字节的随机序列,逐码字检查游程长度不超过 5、以及累计 disparity 始终在 ±1 以内有界。这两条是 DC 平衡的直接判据。
4.3 解码、非法码字检测与 8B 还原
解码比编码多一层校验。收到 10 位码字后,先算它的 disparity 和当前 RD 是否冲突,冲突直接报invalid_code;再从表里反查 x 和 y,拼回 8 位;最后更新 RD。反查用 dict 反向映射即可,把 256 个可能码字全部塞进去,查不到就是非法。
| 校验项 | 判定条件 | 典型原因 |
|---|---|---|
| 游程超限 | 连续相同位 > 5 | 位同步偏移、信道误码 |
| disparity 冲突 | 码字极性与当前 RD 同号 | 码字非法或链路丢位 |
| 反查失败 | 10 位不在合法码字集合内 | 传输错误、未对齐 |
| K 码位置异常 | 数据区内出现控制码 | 协议状态机错乱 |
4.4 和硬件查表实现的对照
FPGA 或 ASIC 里做 8B/10B,常见做法是把 256 项码表烧进 BRAM 或直接综合成组合逻辑的 LUT,一拍出结果,RD 用一个触发器保存。软件模型的价值不在速度,而在于可以逐字节打印中间状态——RD 在哪个字节开始偏离、哪个码字触发了游程违例,硬件波形很难给出这种粒度。做 SerDes 调试时,我一般先让软件模型跑一遍完整激励,把预期码流 dump 出来,再和协议分析仪抓到的实际码流做 diff,定位能快很多。
5. 链路调试中验证 8B/10B 的四个具体手段
5.1 用游程长度统计做第一道筛查
拿到一段抓取的码流,第一件事不是解码,而是统计游程。一段完全合规的 8B/10B 码流里,连续相同位的最大长度严格等于 5。写几行 Python 扫一遍,出现 6 或以上,说明要么抓取时位边界偏了,要么中间丢了采样点。
import itertools def max_run(code_stream): """返回码流中最长的连续相同位长度""" return max(len(list(g)) for _, g in itertools.groupby(code_stream)) stream = ''.join(['0011111010', '0101010101', '1100000101']) print(max_run(stream)) # 应为 5;超过 5 说明码流有问题itertools.groupby按相邻相同字符分组,取组长度最大值。这个方法对没对齐的码流同样敏感——一旦位偏移,原本分散的 1 会连成一片,跑出来的值通常在 8 以上。
5.2 从 RD 失配日志反推问题位置
比游程更细的线索是 RD 序列。把解码过程中每次next_rd的输入输出都打出来,正常链路会看到 RD 在 -1 和 +1 之间规律翻转,且每个非法码字都会被立即标出。如果 RD 在某个字节之后突然卡住不动,说明连续收到了完美平衡的码字,多半是链路进了空闲态;如果某个位置的 RD 跳变和预期差一个符号,往回退一个码字,那个位置就是首错点。
实际调试时,串口日志里刷的通常是「第 N 个码字非法」这类信息,光看编号很难定位。把码字序号、原始 10 位值、当前 RD、期望 RD 四列一起打出来,用 diff 工具和软件模型的输出对齐,能直接看到分叉点。这一步做完,问题基本就落在「链路丢位」还是「编码表不匹配」两个方向上,剩下的验证工作就简单了。
本文还有配套的精品资源,点击获取