第二章:PCIe体系结构——三层架构的职责与协作
2.1 为什么需要三层?
PCIe不是一根简单的总线,它是一套完整的协议栈。我个人刚开始接触时也被吓到——三层?至于吗?后来踩了坑才明白:每一层解决一类问题,缺一不可。
事务层(Transaction Layer)负责“你要干什么”,数据链路层(Data Link Layer)负责“能不能可靠送到”,物理层(Physical Layer)负责“怎么在线上传信号”。你想想看,如果没有数据链路层的重传机制,TLP丢了都不知道,那系统直接死机。
2.2 事务层——TLP是主角
事务层是离软件最近的一层。你从驱动里发起一个MMIO读,事务层就会把它打包成一个Memory Read TLP。TLP格式我给大家拆开看:
| 字段 | 位数 | 说明 |
|---|---|---|
| Fmt | 2 bits | 格式:3DW无数据 / 4DW无数据 / 3DW有数据 / 4DW有数据 |
| Type | 5 bits | 事务类型:MRd, MWr, Cpl, CplD 等 |
| TC | 3 bits | 流量类别(Traffic Class),用于QoS |
| TD | 1 bit | 是否包含TLP Digest(ECRC) |
| EP | 1 bit | 中毒位(Poisoned),标记数据已损坏 |
| Attr | 3 bits | 属性:顺序、宽松排序、ID-based排序 |
| Length | 10 bits | 数据负载长度(以DW为单位) |
| Requester ID | 16 bits | 总线|设备|功能(BDF) |
| Tag | 8 bits | 标签,用于匹配请求与完成 |
| Last DW BE | 4 bits | 最后一个DW的字节使能 |
| 1st DW BE | 4 bits | 第一个DW的字节使能 |
事务层还负责流量控制(Flow Control)。每个VC(Virtual Channel)有独立的信用量机制。发送方不能随意丢包,得先看接收方有没有足够的接收缓冲区。我曾在项目里遇到一个坑:某个EP的FC信用量公告逻辑写死了,导致RC一直拿不到信用,整条链路卡死——调了三天才发现。
2.3 数据链路层——可靠传输的守护者
数据链路层给TLP加了两样东西:序列号(Sequence Number)和LCRC。收到一个TLP后,链路层校验CRC,如果正确就发一个ACK DLLP回去;如果错误就发NAK,要求重传。这个机制叫Go-Back-N ARQ。
DLLP格式和TLP完全不同。DLLP是固定长度(6 bytes),没有地址概念,只在相邻的两个PCIe设备之间交换。常见的DLLP类型:
- ACK/NAK:确认/否定确认
- InitFC1/InitFC2:初始化流量控制信用量
- UpdateFC:更新信用量
- PM_*:电源管理相关
- Vendor_Defined:厂商自定义
数据链路层的重传缓冲区大小有上限。PCIe规范规定重传深度至少是3个TLP(Gen1/Gen2),Gen3以后可以更大。但在做FPGA原型验证时,我习惯把缓冲区开到8个TLP,给自己留点余量。
2.4 物理层——信号到底能不能过?
物理层分两部分:逻辑子层和电气子层。逻辑子层做编码(8b/10b或128b/130b)、加扰、弹性缓冲区;电气子层负责差分信号的驱动和接收。物理层最关键的阶段是链路训练状态机(LTSSM):从Detect到Polling,再到Config,最终进入L0正常工作。
物理层还负责链路宽度协商。比如一个x16的设备插到x8的槽上,物理层通过训练自动降宽。我debug时经常在LTSSM的Recovery状态卡住,通常原因是信号完整性差——眼图没睁开。
// 简单物理层状态机片段(伪代码)
state = Detect;
loop {
switch (state) {
Detect: // 检测接收端是否存在
if (receiver_detected) state = Polling;
break;
Polling: // 位锁定和符号对齐
if (bit_lock && symbol_lock) state = Config;
break;
Config: // 交换链路宽度和速率
if (link_width_negotiated) state = L0;
break;
L0: // 正常操作
// 发送TLP/DLLP
break;
Recovery: // 重新训练
if (recovery_done) state = L0;
break;
}
}
2.5 三层协作:一个读事务的完整旅程
我们来看一个简单的Memory Read事务,从RC到EP再到RC:
- RC的事务层收到软件发起的读请求,构造一个Memory Read TLP(Fmt=2DW无数据,Type=MRd),加上Requester ID、Tag、地址。然后下发给数据链路层。
- 数据链路层在TLP前面加上序列号(比如N),后面加上LCRC。把结果交给物理层。同时把这个TLP放入重传缓冲区(Replay Buffer)。
- 物理层对数据做编码(Gen3使用128b/130b),加扰后通过lane发送。接收端物理层做解码、去扰、弹性缓冲,还原出TLP+DLLP边界。
- EP的数据链路层收到后,校验LCRC。正确则发ACK DLLP回RC,并剥离序列号和LCRC,把TLP往上送。错误则发NAK,RC收到NAK后会从重传缓冲区重新发送从N开始的所有TLP。
- EP的事务层解析TLP,发现是Memory Read,就去访问内部寄存器。然后构造一个Completion with Data TLP(CplD),同样走数据链路层和物理层返回RC。
- RC收到CplD后,匹配Tag,把数据返回给软件。
你想想看,每一步出错都可能让整个系统挂掉。我曾经在调试过程中,看到RC发出MRd后一直等不到CplD,最后发现是EP的BAR空间没初始化(分配了地址但没使能)——这在枚举过程中很常见,我们后面会细讲。
2.6 重要对比:TLP vs DLLP
| 对比项 | TLP | DLLP |
|---|---|---|
| 长度 | 可变(最小3DW,最大4096DW) | 固定6 bytes |
| 传输范围 | 端到端(从发送端到接收端) | 仅相邻设备之间(点对点) |
| 可靠机制 | 依赖数据链路层 | 自身带CRC,且可重传 |
| 用途 | 数据/命令/完成 | 链路管理(FC、ACK、电源管理) |
| 序列号 | 无(由数据链路层添加) | 无 |
2.7 关于VC与TC的补充
事务层里还有一个VC(Virtual Channel)的概念。实际上物理上有多个独立的虚拟通道,每个VC有自己的一组信用量、重传缓冲区。TC(Traffic Class)是用来标记优先级。你可以配置TC到VC的映射,比如TC0走VC0,TC7走VC1。我建议你在做QoS相关设计时,仔细核对RC和EP的TC/VC映射表是否一致——不一致会导致事务被丢弃。
好了,这一章就把三层的基础过了一遍。下一部分我们会深入到具体的枚举流程——这才是面试真正的重头戏。