﻿# PCIe 阶段①：物理上电 → 链路训练 → L0 就绪

> 按下电源键后，PCIe 总线上的**第一件事**是什么？不是 BIOS 扫描设备，不是内核枚举——而是**硬件自动完成的链路训练**。没有任何软件参与，PCIe 两端（Root Complex 的 PHY 和 Endpoint 的 PHY）在几十毫秒内完成从 Detect 到 L0 的全过程。本篇拆解这个"看不见的阶段"——链路训练的每个状态机转换、Link Width 和 Speed 的协商机制、训练失败会怎样、以及 `lspci` 里 `LnkSta` 字段为什么能告诉你物理层问题。

## 零、总览：链路训练在时间线上的位置

```bash
按下电源键
    │
    ▼
┌──────────────────────────────────────────────────────────────┐
│ ① 硬件链路训练 (0~100ms)                                    │
│    纯硬件 LTSSM 自动完成                                     │
│    → 输出: L0 状态 + 协商好的 Width/Speed                   │
│    ← 本篇                                                      │
├──────────────────────────────────────────────────────────────┤
│ ② 固件扫描 Bus 0 + 分配临时 BAR (UEFI/BIOS)                 │
├──────────────────────────────────────────────────────────────┤
│ ③ 内核深度优先枚举 + 建树 (pci_scan_root_bus)               │
├──────────────────────────────────────────────────────────────┤
│ ④ BAR 激活 + 地址解码器生效 + 驱动 probe()                  │
└──────────────────────────────────────────────────────────────┘
```

| 链路训练完成意味着 | 链路训练没做或失败意味着 |
|------------------|------------------------|
| 物理层可以传 TLP | 软件连 Config TLP 都发不出去 |
| LTSSM 停在 L0 状态 | 设备在 `lspci` 里根本看不到 |
| 两端协商好了 Link Width & Speed | 金手指没插紧、信号质量差 |

> **核心结论**：链路训练是所有 PCIe 通信的物理前提。没有它，就没有后面的枚举、BAR 分配、DMA——什么都不会发生。而它完全由硬件自动完成。

---

## 一、核心要点

- **链路训练是纯硬件行为**：PCIe PHY 内部的 LTSSM（Link Training and Status State Machine）自动运行，软件零参与
- **LTSSM 有 11 个状态**，最关键的路径是：Detect → Polling → Configuration → L0
- **协商两个参数**：Link Width（x1/x4/x8/x16）和 Link Speed（2.5/5/8/16/32 GT/s）
- **L0 是正常工作的状态**：可以传输 TLP/DLLP/Ordered Sets
- **训练失败** → Recovery 状态重试 → 降速/降宽 → 最终可能进入 Disabled（设备不可见）

---

## 二、LTSSM 状态机全景

### 2.1 状态机拓扑

```plantuml
@startuml
skinparam shadowing false
skinparam BoxPadding 10
skinparam DefaultFontSize 11

[*] --> DQ : 上电/复位

state Detect {
  state "Detect.Quiet" as DQ
  state "Detect.Active" as DA
  DQ --> DA : 开始检测
  DA --> DQ : 未检测到终端电阻
}

DA --> PA : 检测到接收端
DQ --> DA : 定时重试

state Polling {
  state "Polling.Active" as PA
  state "Polling.Configuration" as PC
  state "Polling.Compliance" as PComp
  PA --> PC : TS1/TS2 交换完成
  PA --> PComp : 进入 Compliance 模式
}

PC --> CLS : 进入配置阶段

state Configuration {
  state "Configuration.Linkwidth.Start" as CLS
  state "Configuration.Linkwidth.Accept" as CLA
  state "Configuration.Lanenum.Wait" as CLW
  state "Configuration.Lanenum.Accept" as CLAc
  state "Configuration.Complete" as CC
  state "Configuration.Idle" as CI
  CLS --> CLA : Lane 协商
  CLA --> CLW
  CLW --> CLAc
  CLAc --> CC
  CC --> CI
}

CI --> L0 : 配置完成

state L0 : 正常工作状态\n可传输 TLP/DLLP\n周期性发 SKP Ordered Set

state Recovery : 链路出错后重新训练
state Disabled : 链路不可用

L0 --> Recovery : 链路出错/信号差
Recovery --> CLS : 重新协商
Recovery --> Disabled : 多次失败

L0 --> L0s : 省电 (一端主动进入)
L0s --> L0 : 有数据时快速唤醒
L0 --> L1 : 省电 (两端协商进入)
L1 --> Recovery : 唤醒

@enduml
```

**主线路径**：Detect.Quiet → Detect.Active（检测到终端电阻）→ Polling.Active（bit lock / symbol lock）→ Polling.Configuration → Configuration.Linkwidth.Start（协商 Width/Speed）→ Configuration.Lanenum → Configuration.Complete → Configuration.Idle → **L0**（正常工作）。这条线是每次上电/热插拔必须走通的，中间任何一步失败都会回到起始状态重试。

**省电旁路**：L0 是"全速工作"，但空闲时不需要一直开着全部模拟电路。L0s 是单端省电（一端主动，退出 ~几十 ns），L1 是双端省电（两端协商，退出 ~几 μs）。唤醒时必须经过 Recovery 重新锁定 bit/symbol 后再回 L0。

**容错回退**：L0 运行中信号质量下降（误码率过高、链路不稳定）→ 进入 Recovery → 重新训练 → 若成功回到 Configuration 重新协商 → 若反复失败且超阈值 → Disabled（链路关闭，lspci 不可见）。

### 2.2 11 个状态逐一解释

| 状态 | 子状态 | 做什么 | 时长 |
|------|--------|--------|:----:|
| **Detect** | Quiet / Active | 发送端检测对端是否有终端电阻（Receiver Detection）。PCIe 规范规定每个 Lane 的接收端必须在差分对上挂一个 50Ω 终端到地。发送端给差分对充电、看放电曲线——有终端电阻说明对面插了设备 | ~12ms |
| **Polling** | Active / Configuration / Compliance | 两端交换 TS1/TS2 Ordered Sets（训练序列），确认链路可用、极性不反转、Lane 不交叉。Compliance 子状态用于测试设备是否符合规范 | ~几 ms |
| **Configuration** | Linkwidth.Start / Accept / Lanenum.Wait / Accept / Complete / Idle | 协商 Link Width 和 Lane 编号。TS1/TS2 里带了"我支持 x16"和"我希望用 x8"等信息。两端取**最小值**。Lane 编号也在此阶段确定（Lane 0 必须是第一个可用的 Lane） | ~几 ms |
| **L0** | — | 正常工作状态。TLP/DLLP 可以正常收发。周期性发 SKP Ordered Set 做时钟补偿（两端晶振不可能完全同步） | 一直 |
| **L0s** | — | 低功耗状态，一端主动进入。退出延迟 ~几十 ns（比 L1 快得多） | 空闲时 |
| **L1** | — | 更深省电，两端协商后进入。退出延迟 ~几 μs | 深度空闲 |
| **Recovery** | RcvrLock / RcvrCfg / Idle | 从 L0s/L1 退出，或链路出错后重新训练。重新做 bit lock 和 symbol lock | ~几 ms |
| **Hot Reset** | — | 带内复位（通过 TS1 发复位命令），不改变 Link Width/Speed | — |
| **Disabled** | — | 链路不可用，发送端进入低阻抗状态。设备在 `lspci` 里看不到 | 直到重新上电 |
| **Loopback** | Entry / Active / Exit | 测试模式，发送端的数据环回到接收端（不经过远端 PHY） | 测试时 |
| **Compliance** | — | 一致性测试模式，设备按规范发送预定义码型，用于电气合规测试 | 测试时 |

---

## 三、关键协商过程详解

### 3.1 Link Width 协商：为什么 x16 插槽可能跑在 x8

链路训练过程中，两端在 Configuration 阶段交换 TS1/TS2，其中携带了**支持的 Lane 数量**：

```plantuml
@startuml
skinparam shadowing false
skinparam DefaultFontSize 11
skinparam participant {
  Padding 50
}

participant "RC 端口\n(x16 插槽, 16 条 Lane)" as RC
participant "Endpoint\n(NVMe, x8 PHY)" as EP

RC -> EP : TS1 Ordered Set\n"我支持 x16, 实际引出 16 Lane"
EP -> EP : 检查对方 Lane 数\n对比自身能力 (x8)
EP --> RC : TS1 Ordered Set\n"我支持 x8"
RC -> RC : <b>取最小值 → x8</b>

note over RC, EP
  <b>协商结果: Width = x8</b>
  · 即便是 x16 插槽, EP 只有 x8 PHY → 跑 x8
  · 同理 Speed 协商也是取两者最小值
end note

@enduml
```

**实际降宽的常见原因**：

| 原因 | 说明 |
|------|------|
| **Endpoint 只做了 x8 PHY** | 很多 NVMe SSD 物理金手指是 x4，即便插在 x16 槽里也跑 x4 |
| **CPU PCIe 通道不足** | CPU 总 Lane 数固定，多个设备分摊后每个拿不到完整宽度 |
| **Bifurcation 配置错误** | BIOS 里把 x16 槽拆成 x8x8 或 x4x4x4x4，每段 Lane 数减半 |
| **信号质量问题** | 差分对阻抗不匹配、PCB 走线过长，训练时某些 Lane 反复失败被排除 |
| **PCB 布线偷工减料** | 物理插槽是 x16，但主板上只走了 x8 的差分对（杂牌主板常见） |

```bash
# 查看实际协商结果
lspci -vvv -s 01:00.0 | grep -E "LnkSta|LnkCap"
# LnkCap: Port #0, Speed 16GT/s, Width x16   ← 能力
# LnkSta: Speed 8GT/s, Width x8              ← 实际 ← 降速降宽了!
```

### 3.2 Link Speed 协商：为什么 Gen4 设备可能跑在 Gen3

Speed 协商在 Polling 和 Configuration 阶段通过 TS1/TS2 的 Data Rate Identifier 字段完成：

```plantuml
@startuml
skinparam shadowing false
skinparam DefaultFontSize 11
skinparam participant {
  Padding 50
}

participant "RC\n(支持 Gen4)" as RC
participant "Endpoint\n(支持 Gen4)" as EP

RC -> EP : TS1 / TS2 Ordered Set\n"Data Rate Identifier = 4\n最高支持 Gen4 (16 GT/s)"
EP -> EP : 检查对方速率\n对比自身能力 (Gen4)
EP --> RC : TS1 / TS2 Ordered Set\n"Data Rate Identifier = 4\n最高支持 Gen4 (16 GT/s)"
RC -> RC : <b>取双方交集的最小值 → Gen4</b>

note over RC, EP
  <b>协商结果: Speed = Gen4, 16 GT/s</b>
  · 两端都支持 Gen4 → 跑 Gen4
  · 若 RC Gen4, EP Gen3 → 最终跑 Gen3 (向下兼容)
end note

@enduml
```

**但如果信号质量不够**：链路训练在某个 Speed 上反复失败 → Recovery → 降一档 Speed 重试 → 最终稳定在较低 Speed。

```bash
# 查看协商速度
lspci -vvv -s 01:00.0 | grep "LnkSta:"
# LnkSta: Speed 8GT/s (downgraded), Width x8 (downgraded)
#                                  ↑ 出现了 downgraded!
```

### 3.3 Lane Reversal 与 Polarity Inversion

PCIe 物理层有两个"纠错"机制，在 Polling 阶段自动处理：

| 机制 | 问题 | 硬件怎么处理 |
|------|------|------------|
| **Lane Reversal** | PCB 走线把 Lane 0 接到了对端 Lane 3 | TS1 中带 Lane 编号，接收端识别到编号倒序后自动映射 |
| **Polarity Inversion** | 差分对的 P/N 接反了 | 接收端检测到 D 码字极性不对，自动翻转 |

> 这两个机制让 PCB 设计更自由——走线时 Lane 顺序和差分极性都可以任意，PHY 在训练阶段自动纠正。

---

## 四、链路训练的硬件参与者

```plantuml
@startuml
skinparam shadowing false
skinparam BoxPadding 20
skinparam ParticipantPadding 60

rectangle "Root Complex (RC)" as RC {
  rectangle "PCS\n(Physical Coding Sublayer)" as RC_PCS
  rectangle "PMA\n(Physical Medium Attachment)" as RC_PMA
}

rectangle "Endpoint (EP)" as EP #line.dashed {
  rectangle "PCS\n(Physical Coding Sublayer)" as EP_PCS
  rectangle "PMA\n(Physical Medium Attachment)" as EP_PMA
}

RC_PCS -down-> RC_PMA : 8b/10b 或 128b/130b 编码, 并行→串行
RC_PMA -right-> EP_PMA : 差分信号对 (TX± & RX±) × N Lane
EP_PMA -up-> EP_PCS : 串行→并行, 解码

note bottom of RC
  <b>LTSSM 位于 PCS 内部</b>
  训练期间: 生成/解析 TS1/TS2 Ordered Sets
  → 控制状态机跳转 → 上报结果给上层
end note

note bottom of EP
  <b>EP 侧也有独立 LTSSM</b>
  两端 LTSSM 通过 TS1/TS2 中的控制位同步状态跳转
end note

@enduml
```

物理层分两个子层：

| 子层 | 全称 | 职责 |
|------|------|------|
| **PCS** | Physical Coding Sublayer | 8b/10b(Gen1/2) 或 128b/130b(Gen3+) 编解码、Elastic Buffer（跨时钟域）、**LTSSM 状态机** |
| **PMA** | Physical Medium Attachment | SerDes（并串/串并转换）、差分驱动/接收、时钟恢复（CDR） |

> LTSSM 位于 PCS 层。两端各有独立 LTSSM，通过 TS1/TS2 Ordered Sets 中嵌入的控制位来同步状态跳转。

---

## 五、训练失败的排查

### 5.1 从 OS 侧看到的症状

| 症状 | 含义 |
|------|------|
| `lspci` 看不到设备 | 链路训练失败，LTSSM 停在 Detect 或 Polling，连 Config TLP 都发不了 |
| `lspci -vvv` 显示 `LnkSta: Speed ... (downgraded)` | 高速协商失败，降速跑了 |
| `dmesg` 里 `link training error` 或 `PCIe link down` | 训练中断或反复 Recovery |
| 设备偶尔出现、偶尔消失 | 信号边缘化（margin 不够），温度变化导致偶尔训练失败 |

### 5.2 常见原因速查

| 原因 | 怎么确认 | 怎么修 |
|------|---------|--------|
| **金手指氧化/接触不良** | 换槽位后恢复正常 | 用橡皮擦擦金手指、重新插紧 |
| **电源不足** | GPU 等高功率设备训练失败，换大电源后正常 | 换电源或加辅助供电 |
| **信号完整性问题** | 特定主板 + 特定设备组合失败，其他组合正常 | 检查 PCB 差分阻抗、走线长度匹配 |
| **BIOS PCIe 配置** | 进 BIOS 看 PCIe Speed 是否被手动设低 | 恢复 Auto/Gen4 |
| **Bifurcation 错误** | x16 槽配置成了 x4x4x4x4，但插的是 x8 设备 | BIOS 里改回 x16 或 x8x8 |
| **Retimer/Redriver 故障** | 长距离线缆或背板连接，中间芯片挂了 | 换线缆或中间芯片 |
| **时钟源问题** | 共用时钟（Common Clock）配置错误，两端 REFCLK 不同源 | 检查 SRIS（Separate Refclk Independent SSC）配置 |

### 5.3 排查命令

```bash
# 1. 看设备是否存在
lspci -t -vv

# 2. 看具体设备的链路状态
lspci -vvv -s 01:00.0 | grep -E "LnkSta|LnkCap|LnkCtl"

# 3. 看内核报的链路错误
dmesg | grep -iE "pcie.*link|pci.*train|pcie.*error|pcie.*down"

# 4. 看 ASPM（省电）状态——可能因 ASPM 切换导致 Recovery
lspci -vvv -s 01:00.0 | grep ASPM

# 5. 看所有 PCIe AER（高级错误报告）
dmesg | grep -i "AER"
```

---

## 六、与后续阶段的衔接

链路训练完成后（L0 状态），下一个问题是：**CPU 怎么知道总线上插了设备？**

→ 进入 [pcie-firmware-enum.md](./pcie-firmware-enum.md)（阶段②：固件枚举）

---

## 七、与现有文档的关系

- [pcie.md](./pcie.md) §二.3 — 物理层概述（本篇是该节的深度展开）
- [pcie-firmware-enum.md](./pcie-firmware-enum.md) — 链路训练完成后，固件怎么用 Config TLP 扫描 Bus 0
- [pcie-pre-enumeration.md](./pcie-pre-enumeration.md) — RC/BDF/BAR 在树构建前的硬件工作原理

---

## 八、一句话总结

> **链路训练是 PCIe 通信的物理前提——纯硬件 LTSSM 自动运行，从 Detect（检测对端电阻）→ Polling（交换 TS1/TS2）→ Configuration（协商 Width/Speed）→ L0（就绪），几十毫秒内完成，软件零参与。训练失败或降速降宽是 `lspci` 看不到设备或性能打折的最底层原因。**

