链路层基础:原语、帧结构与 8b/10b 编解码

这一章,我们深入链路层的核心。很多人觉得链路层就是“打包发送”,其实不然。我刚开始接触 SATA 协议时,就被这些原语搞得头大——明明是“原始语”,为什么还分这么多类型?后来踩了几次坑才明白,链路层是数据能否稳定传输的命门。

1. 原语:链路层的“摩尔斯电码”

原语(Primitive)是 SATA 链路层最小的通信单元。它不像帧那样携带上层数据,而是负责链路状态同步、流量控制和帧边界识别。简单说,原语是协议引擎之间的“旗语”。

每个原语长 4 字节(1 个 Dword)。前 3 个字节是固定编码,最后 1 个字节用于区分具体类型。我常用的原语就这几种:

原语类型 编码值(十六进制) 核心作用
ALIGNp 7B4A7AB9h 字节对齐、速率协商
SYNCp B5B595B5h 空闲状态、保持链路活动
X_RDYp B5B585B5h 发送就绪、发起帧传输
R_RDYp B5B5ADB5h 接收就绪、应答对方
SOFp B5B5A5B5h 帧起始标记
EOFp B5B5A5B5h 帧结束标记
HOLDp B5B585B5h 暂停发送、等待处理
PMREQp B5B5AAB5h 电源管理请求
个人习惯:调试链路训练时,我最先抓的就是 ALIGNp 和 SYNCp 的交互序列。如果 ALIGNp 连续出现 4 次以上还没收到对方的 ALIGNp 应答——那基本是物理层或时钟问题,不用怀疑链路层。

原语的发送优先级是有讲究的。我记得有一次联调,对方设备不断发 HOLDp 打断数据流,我这边以为链路断了,疯狂复位。后来才发现,HOLDp 的优先级高于数据帧,这是正常的流控行为。所以,优先级顺序:ALIGNp > 控制原语 > 数据帧。

2. 帧结构:数据怎么打包?

SATA 帧由帧头(FIS Header)和帧内容(Payload)组成,外面再包上原语“壳”。来看看一个典型的数据帧长什么样:

| SOFp | 帧头(4 Dwords) | 数据(用户数据 + CRC) | EOFp |
|------|-------------------|------------------------|------|
| 1 DW |       4 DW       |         N DW           | 1 DW |
  • SOFp:告诉接收端“帧开始了”,接收端才开始接收数据。
  • 帧头:描述帧类型、端口号、数据长度等。比如 DMA 写操作,帧头里就写“我要发多少字节”。
  • 数据区:实际用户数据 + 32 位 CRC 校验。CRC 覆盖从帧头到数据末尾。
  • EOFp:帧结束,接收端开始校验 CRC。
避坑提示:我曾经遇到一个 Bug,发送端在 EOFp 之后又多发了一个字节。结果接收端把下一个 SOFp 当成了数据,整个链路卡死。记住:EOFp 之后必须是原语或下一个 SOFp,不能有“残余”。

帧间隔至少是 12 个 Dwords 的空闲原语(SYNCp)。为什么是 12?给接收端足够的处理时间。实际项目中,我建议至少留 16 个 Dwords,兼容性会好很多。

3. 8b/10b 编码:为什么需要它?

物理层传的是差分信号,但信号本身没有时钟。怎么保证接收端能准确采样?8b/10b 编码就是解决方案。

简单说,8b/10b 把 8 位数据映射成 10 位符号。映射规则保证:

  • DC 平衡:10 位符号中 1 和 0 的数量差不超过 2。这对交流耦合的 SerDes 链路至关重要——否则电容会慢慢“漂移”到饱和,信号就坏了。
  • 跳变足够:编码后不会出现连续 5 个相同位,保证接收端能从信号边沿提取时钟。

编码表有两张:RD+ 和 RD-(运行差异,Running Disparity)。发送端会根据当前差异值选择正或负表,动态调整平衡。当初设计这个算法的人,真是个天才。

你想想看:如果传输 0x00 这个字节,原始二进制是 0000_0000,8b/10b 把它编码成 10'h? 结果是 10'b100111_0100 或相反符号。具体编码表可以在 SATA 规范 Annex A 里查到。

4. 编解码实战:控制符号

除了数据符号,8b/10b 还有 12 个控制符号(K 符号)。原语中的 ALIGNp、SYNCp 等,就是用 K 符号+数据符号拼接的。最常见的 K 符号是 K28.5,它的 10 位编码是 001111_1010 或 110000_0101。

为什么 K28.5 这么重要?因为它包含了 5 个连续“1”或“0”的序列,这在正常数据符号里不会出现。接收端看到这种“非法”模式,就知道这里是一个符号边界。

我调试时,经常用逻辑分析仪抓 10 位原始信号。先找 K28.5,然后以它为基准分割成 Dword,再识别原语。嗯,这个技巧很土,但管用。

5. 本章知识体系一览

下面这张图,把原语、帧结构、8b/10b 编码的关系串起来了。从发送端到接收端,数据到底经历了哪些环节?

链路层数据流向:从原语到 8b/10b 编码 1. 原语生成 SYNCp / ALIGNp / X_RDYp 优先级判定 + 序列拼接 2. 帧封装 SOFp + 帧头 + Payload + CRC 帧间隔 ≥ 12 DW (SYNCp) 3. 8b/10b 编码 数据/控制符号映射 RD+ / RD- 动态选择 4. 8b/10b 解码 K28.5 边界定位 运行差异校验 5. 原语识别 + 帧解析 SOFp/EOFp 定位帧边界 CRC 校验 → 数据交付 6. 链路状态管理 HOLDp 流控 / PMREQp 电源 链路训练完成标志 差分对传输 核心结论: 原语负责控制与同步,帧负责数据封装,8b/10b 保证物理传输质量。 三者缺一不可——没有原语就“说不了话”,没有编码就“听不见”。

图中从“原语生成”到“8b/10b 编码”,再到接收端对称处理,最后回到“链路状态管理”,这就是一整套链路层数据流。你要是能把这 6 个节点的交互逻辑吃透,链路层就算过关了。

调试建议:初学者最容易卡在“原语序列”上。我习惯的做法是:先用逻辑分析仪抓一段 ALIGNp 序列,看它是不是连续 4 次以上;然后找 X_RDYp → R_RDYp 的握手信号;最后再看帧的 SOFp/EOFp 是否成对出现。三步走,链路就清晰了。