链路层基础:原语、帧结构与 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 | 电源管理请求 |
原语的发送优先级是有讲究的。我记得有一次联调,对方设备不断发 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。
帧间隔至少是 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)。发送端会根据当前差异值选择正或负表,动态调整平衡。当初设计这个算法的人,真是个天才。
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 编码”,再到接收端对称处理,最后回到“链路状态管理”,这就是一整套链路层数据流。你要是能把这 6 个节点的交互逻辑吃透,链路层就算过关了。