2、PCIe体系结构:层次结构(事务层、数据链路层、物理层)、组件(根复合体、交换器、端点)
好,咱们今天聊聊PCIe的体系结构。说实话,很多工程师刚接触PCIe时,最头疼的就是搞不清它到底是怎么一层层工作的。我当年也一样,看着协议文档里那三层结构,脑子里全是问号。后来在项目中调了无数次链路,才慢慢摸清楚门道。
PCIe的体系结构,说白了就是三个层次加三个组件。三个层次是:事务层、数据链路层、物理层。三个组件是:根复合体、交换器、端点。咱们一个一个来拆解。
2.1 三层结构:数据是怎么“打包”的?
你想想看,数据从CPU发到设备,中间要经过多少道工序?PCIe的设计思路很巧妙——它把整个通信过程拆成了三个独立的层。每一层只管自己的事,互不干扰。这样做的好处是,哪一层出了问题,你只需要盯着那一层查就行。
2.1.1 事务层(Transaction Layer)
事务层是最靠近软件的一层。它的任务很简单:把软件要读写的请求,打包成“事务层包”(TLP)。
我个人习惯把TLP想象成一个快递包裹。包裹外面贴着地址(路由信息),里面装着你要送的东西(数据)。事务层只负责打包和拆包,至于这个包裹怎么送到目的地,它不管。
TLP的核心组成:
- 包头(Header):包含事务类型、长度、地址等信息。我见过不少新手在这里栽跟头——包头里的长度字段填错了,导致接收方一直收不齐数据。
- 数据负载(Data Payload):实际要传输的数据,最大可达4KB。
- ECRC(端到端CRC校验):可选字段,用于端到端的数据完整性校验。我在项目中遇到过ECRC没使能,结果数据在中间被篡改了都不知道。
事务层还负责处理“事务排序”。PCIe允许乱序传输,但有些事务必须按顺序完成。比如,你先写了一个寄存器,然后又读同一个寄存器,那读操作必须等写操作完成后再执行。嗯,这里要注意,乱序虽然能提高性能,但也容易引入bug。
2.1.2 数据链路层(Data Link Layer)
数据链路层是中间层,它负责确保TLP能可靠地从一端传到另一端。说白了,它就是快递公司的中转站——检查包裹有没有破损,如果破损了就要求重新发一个。
数据链路层主要干两件事:
- 添加序列号和LCRC:每个TLP在发送前,数据链路层会给它加上一个序列号(Sequence Number)和链路层CRC(LCRC)。接收方收到后,先检查LCRC对不对,再检查序列号是不是连续的。如果发现丢包或错包,就要求重传。
- 流量控制(Flow Control):数据链路层会管理接收方的缓冲区。发送方不能一股脑地发数据,得先问问接收方:“你还有空间吗?”接收方说“有”,才能继续发。我曾经在调试一个FPGA的PCIe接口时,发现链路老是卡住,查了半天才发现是流量控制逻辑写错了,接收方明明还有空间,却告诉发送方“满了”。
我的经验:调试链路层问题时,我最喜欢看的是“ACK/NAK”机制。如果发现重传次数特别多,那大概率是物理层信号质量有问题,或者时钟抖动太大。别一上来就怀疑协议栈,先看看眼图。
2.1.3 物理层(Physical Layer)
物理层是最底层,它负责把数据链路层送过来的包,变成电信号或光信号,从一根根差分线上发出去。物理层又分为两个子层:逻辑子层和电气子层。
逻辑子层:负责数据的编码和解码。PCIe 1.0和2.0用的是8b/10b编码,每10个比特里只有8个是有效数据,效率80%。到了PCIe 3.0,换成了128b/130b编码,效率提升到98.5%。嗯,这里要注意,不同代的PCIe,物理层编码方式不一样,链路训练时得先协商好。
电气子层:负责信号的发送和接收。包括差分驱动器、接收器、阻抗匹配等。我记得有一次,一个板卡的PCIe链路死活训练不过去,用示波器一看,发现发送端的信号幅度只有200mV,远低于规范要求的800mV。最后查出来是电源供电不足,给PHY供电的LDO输出纹波太大。
避坑指南:我曾经在调试一块高速背板时,发现PCIe链路偶尔会掉线。查了三天,最后发现是物理层的“去加重”参数没配好。去加重是为了补偿高频信号在传输线上的衰减,但不同长度的走线需要不同的去加重值。如果你发现链路不稳定,先检查一下物理层的配置寄存器。
2.2 三大组件:谁是谁?
讲完三层结构,咱们再看看PCIe体系里的三个核心组件。这三个组件构成了整个PCIe拓扑。
2.2.1 根复合体(Root Complex, RC)
根复合体是PCIe体系的老大。它连接着CPU、内存和PCIe总线。所有从CPU发出来的请求,都要经过根复合体才能到达PCIe设备。
根复合体通常集成在CPU或芯片组里。比如Intel的CPU里,根复合体就负责把CPU的DMI总线转换成PCIe总线。根复合体可以包含多个PCIe端口,每个端口可以连接一个交换器或端点。
我个人习惯把根复合体想象成一个“交通枢纽”。它负责把CPU的读写请求转换成PCIe事务,然后分发到各个端口。同时,它也负责把PCIe设备的中断请求转发给CPU。
2.2.2 交换器(Switch)
交换器是PCIe体系里的“路由器”。它的一端连接根复合体,另一端连接多个端点。交换器的作用就是把来自根复合体的数据包,转发到正确的端点去。
交换器内部其实是由多个“虚拟PCI桥”组成的。每个虚拟桥对应一个下游端口。交换器根据数据包里的地址信息,决定把包转发到哪个端口。
交换器的关键特性:
- 点对点通信:交换器支持不同端点之间的直接通信,不需要经过根复合体。比如,两个FPGA通过交换器互相传数据,CPU可以完全不管。
- 流量隔离:交换器可以隔离不同端口的流量。一个端口出问题,不会影响其他端口。我在项目中遇到过交换器的一个端口因为电气问题频繁复位,但其他端口完全不受影响。
- 广播与多播:交换器支持将数据包广播到所有下游端口,或者多播到指定的几个端口。
2.2.3 端点(Endpoint, EP)
端点是PCIe体系里的“终端设备”。它可以是显卡、NVMe SSD、网卡、FPGA加速卡等等。端点只负责接收和发送数据,不负责转发。
端点分为两种:
- 功能端点(Function):一个物理设备可以包含多个功能。比如,一个网卡可以同时提供以太网和iSCSI功能,每个功能都有自己的配置空间。
- RCiEP(Root Complex Integrated Endpoint):直接集成在根复合体里的端点,不需要经过交换器。比如,CPU内部集成的GPU或USB控制器。
端点最核心的部分是“配置空间”。每个端点都有256字节的配置空间,里面包含了设备ID、厂商ID、BAR(基地址寄存器)等信息。系统启动时,BIOS或操作系统会通过配置空间来枚举所有设备,并分配内存地址和中断号。
调试技巧:如果你怀疑某个端点有问题,第一步就是读它的配置空间。用lspci命令(Linux)或者设备管理器(Windows)看看设备ID对不对,BAR地址有没有分配成功。我遇到过好几次,端点配置空间里的BAR大小写错了,导致系统分配的内存地址不对,驱动一加载就崩溃。
2.3 三层与三组件如何协同工作?
咱们用一个实际场景来串一下:CPU要从NVMe SSD里读一个4KB的数据块。
- 事务层:CPU发出一个内存读请求。根复合体的事务层把这个请求打包成一个“读请求TLP”。TLP的包头里写着目标地址(对应SSD的BAR空间),数据长度是4KB。
- 数据链路层:根复合体的数据链路层给这个TLP加上序列号和LCRC,然后交给物理层。
- 物理层:根复合体的物理层把TLP编码成串行比特流,通过差分线发出去。
- 交换器:交换器的物理层收到信号,解码后交给数据链路层。数据链路层检查LCRC和序列号,没问题就交给事务层。事务层解析包头,发现这个包是发给下游某个端口的,于是转发过去。
- 端点(SSD):SSD的物理层收到信号,经过数据链路层校验,最后事务层把TLP拆开,取出读请求。SSD的控制器从NAND Flash里读出数据,然后打包成一个“完成TLP”(Completion TLP),原路返回给CPU。
整个过程看起来复杂,但每一层各司其职,配合得天衣无缝。你想想看,如果没有这种分层设计,任何一个环节出了问题,你都得从头查到尾,那得多痛苦?
总结一下:
- 事务层:负责打包和拆包,处理事务排序。
- 数据链路层:负责可靠传输,处理重传和流量控制。
- 物理层:负责信号收发,处理编码和电气特性。
- 根复合体:连接CPU和PCIe总线,是数据流的源头和终点。
- 交换器:扩展PCIe端口数量,实现点对点通信。
- 端点:实际的功能设备,提供各种I/O能力。
下一章,咱们聊聊PCIe的枚举和配置过程。这可是系统启动时最关键的一步,也是很多问题的根源。到时候我会分享一些我在调试枚举问题时的实战经验。