同很多刚开始接触PCIe的同学一样,我最早看到事务层包、数据链路层包、物理层包这堆概念的时候,脑子里只有一个想法:不就是发个数据吗,搞这么复杂干什么。但等到真正去调FPGA的PCIe接口、去排查一次DMA传输失败、去分析协议分析仪抓回来的报文时,才发现这三种包各有各的职责,谁也替代不了谁。市面上讲PCIe协议的书不少,但大多要么太硬核、全是Spec术语,要么太浅、只告诉你“有这三种包”就结束了。这篇笔记是我自己啃Spec、调板子、抓包过程中的一份整理,核心围绕TLP、DLLP、PLP三种数据包的格式、作用、流程展开,同时会把它们和实际现象串起来讲。无论你是刚准备入门PCIe、正在做驱动开发,还是在用FPGA做PCIe相关的设计,这篇都应该能帮你把这块的知识拼图补完整。
1. 分层架构与三种包的定位
1.1 三层结构到底在干什么
PCIe采用的是分层协议架构,从逻辑上分为事务层(Transaction Layer)、数据链路层(Data Link Layer)和物理层(Physical Layer)。一颗PCIe设备往总线上发数据,并不是软件把数据往寄存器里一写就完事,而是先由事务层把软件要读写的请求打包成TLP,再往下交给数据链路层,数据链路层加上校验和重传管理信息后变成DLLP的一部分,最后物理层把数据包编码成高速差分信号发出去。
接收方向则完全相反。物理层先把串行bit流恢复出来,数据链路层校验完整性并做重传处理,事务层才解析真正读写的地址、数据和消息。
这段流程听起来像一条流水线,它也确实借鉴了流水线的思想。分层的核心目的是让每一层只关心自己的事。事务层不需要管信号怎么编码、怎么对齐;物理层也不需要知道这个报文是读内存还是配置空间。层与层之间通过确定的接口协议通信,这样某个层内部做优化和升级,就不会波及整个系统。举一个最直观的例子:PCIe从Gen1到Gen5,每一代的编码方式、速率都在变,但TLP的格式几乎没有大改,原因正是软件看到的接口被事务层稳定地保护住了。
1.2 三种包的边界与配合关系
三种包的分工可以用一句话概括:TLP是“业务数据”,DLLP是“管理数据”,PLP是“物理维护数据”。
TLP用来承载真正的读写请求、完成应答、消息事件等。比如CPU要读取显卡配置空间里的某个寄存器,事务层会生成一个Configuration Read TLP,由链路层和物理层送出去。DLLP则是收发双方的数据链路层之间私底下传递的控制报文,例如确认收到了哪个TLP(ACK/NAK)、通告接收缓冲区还剩多少空间(流控更新)、电源管理相关的指令等。PLP存在于物理层内部以及相邻PCIe设备物理层之间,它并不携带业务数据,而是承担链路初始化和维护任务——比如链路训练时双方交换的TS1/TS2有序集、为了补偿时钟频偏插入的SKP有序集、链路进入低功耗状态时的Power Management指令等。
从“谁产生、谁消费”的角度来看,TLP的源和目的地是设备内部的事务层,中间的数据链路层和物理层只负责搬运;DLLP的源和目的地是链路两端的数据链路层;PLP则完全属于物理层自治的范围。搞清楚这个边界,遇到抓包数据时才能迅速判断“这个报文是哪个层的、什么用途、是否值得关注”。
2. TLP深度拆解
2.1 TLP的总体格式
TLP是PCIe协议里最重要、信息量最大的一类包。它在事务层生成,通过数据链路层转发到物理层发送。一个TLP由三大部分组成:Header(包头)、Data Payload(数据负载,可选)和可选的TLP Digest(即ECRC)。
Header固定为12字节或16字节,取决于事务类型和地址格式。如果是带数据的写请求、读完成等,它的Header里还会通过双字(DW)计数来标明Data Payload的实际长度。Header中包含的关键字段通常有:Fmt(格式,区分是否有数据负载、Header是否为扩展格式)、Type(类型,区分Memory、IO、Configuration、Message等)、TC(流量类别)、TD(是否带ECRC)、Attr(属性,如无嗅探、排序等)、Length(以双字为单位的数据负载长度)、Requester ID(发送方总线号-设备号-功能号)、Tag(事务标签)、地址或完成状态等信息。
字段特别多,刚开始很容易记混。我自己的记忆方法是先分“骨架”再记“肉”:先记住最前面的Fmt和Type决定了这个包是干什么的,Length决定了包多大,Requester ID和Tag决定了它是“谁发起的第几号请求”,Completer ID则决定了“该由谁来回这个包”。抓住这四个骨架之后,再去看地址、状态等细节就不容易乱了。
一个典型的带数据的64位地址Memory Write TLP的Header布局大概是这样的:
| 字节偏移 | Bit 31-24 | Bit 23-16 | Bit 15-8 | Bit 7-0 |
|---|---|---|---|---|
| 0 | Fmt | Type | TC | Length |
| 4 | Attr | 无嗅探等 | Request ID的高16位(总线/设备/功能) | Tag/Last DW BE |
| 8 | 地址[31:2] | 地址[63:32] | ||
| ... | 数据负载(Data Payload) |
2.2 常见的TLP类型与路由方式
TLP按作用可以分为几大族:Memory读写、IO读写(新设备已不推荐用IO空间,但规范仍保留兼容性)、Configuration读写(用于枚举配置空间)、Completion(完成包,用于响应之前的读请求)、Message(消息,用于中断、电源管理等事件通知)。
每一种TLP都有对应的路由方式。PCIe支持三种TLP路由机制:
- 地址路由:Memory和IO请求根据地址字段进行路由,桥设备解析地址范围来决定是否转发。
- ID路由:Completion和Configuration请求使用BDF(Bus/Device/Function Number)来路由。比如CPU枚举设备时,配置读写TLP的ID就指向目标设备所在的Bus、Device和Function。
- 隐式路由:Message类型不需要携带目标地址或ID,由接收方根据消息类型来自行处理。
这三种路由方式在实际系统中配合使用。以系统软件枚举PCIe设备为例,处理器会先发起Configuration Read TLP,按ID路由找到目标设备,读取其Vendor ID、Device ID、BAR寄存器等。接着软件配置BAR地址,之后才能发起Memory地址路由的读写TLP来访问设备的MMIO空间。
有一个很容易被忽略的点:Completion包必须原路返回,也就是必须记录它的“来源包”所用的Route和Tag,这样发起方才能把完成包和之前发出的请求对上号。在调试乱序完成导致驱动卡死的问题时,往往就是Tag管理出了问题。
2.3 Flow Control与TLP发送窗口
事务层向外发TLP前,必须先做流量控制(Flow Control),这是PCIe保证可靠传输的机制之一。简单说,链路上每个接收方都会在内部维护若干个虚拟通道(VC,Virtual Channel,最常见的是VC0)的接收缓冲区,并通过数据链路层周期性地把“我还有多少空闲空间”告诉对端。发送方只有在确认对端缓冲区能装得下整个TLP时,才会真正把TLP发出去。
Flow Control的单位是“信用”(Credit)。每种TLP类型会分成Posted、Non-Posted、Completion三类信用池,每类再按Header和数据部分分别管理。Posted事务(如Memory Write、Message)发出后不需要对端回复,Non-Posted事务(如Memory Read、Configuration Read)必须得到Completion,Completion本身又占一类信用。为什么要这么分?因为不同事务对缓冲区资源的消耗和对系统可靠性的要求不一样。如果一个接收端的Non-Posted缓冲区满了,它仍然可以接收Posted数据,这样写操作不会因为读操作拥塞而不能进行,系统不至于因为一个方向堵死而完全卡住。
2.4 为什么要有Completion机制
掌握Completion机制,很大程度上就掌握了PCIe事务层设计的精髓。PCIe的事务分为Posted和Non-Posted两类。Posted事务发出去就不用管结果了,比如Memory Write、Message。Non-Posted事务要求对端必须送回一个Completion,告诉发起方“我收到了、结果如何”,最典型的是Memory Read和Configuration Read。
这个设计很像是寄快递。普通快递(Posted)你寄出去就不管了,丢了算快递公司的;挂号信(Non-Posted)则必须要求收件人签收回执,签收信息就是Completion。PCIe之所以区分这两类,是为了在性能(写操作可以疯狂流水线化,不等待返回)和可靠性(读操作必须知道数据或错误状态)之间做平衡。
Completion包中带有Completer ID、Completion Status和Requester ID、Tag等。状态字段区分Successful Completion、Unsupported Request、Completer Abort等。我们做驱动调试时,最常看到的问题就是读某个BAR地址返回全F,这往往是设备没有正确完成应答,或者返回的Completion Status是UR(Unsupported Request)。
2.5 TLP的CRC与完整性保障
TLP在事务层生成时,可以选择性地带一个ECRC(End-to-end CRC),它由发送方事务层基于TLP Header和Data计算,接收方事务层负责校验,中间经过交换器(Switch)时不会被重新计算。这样一来,即使Switch内部的某段路径出了问题,接收端也能通过ECRC发现端到端的数据损坏。
而在每个Link上传输时,发送方的数据链路层还会给TLP追加一个LCRC(Link CRC)。LCRC由数据链路层计算,接收方的数据链路层校验,每经过一个Switch的一段Link,都会被重新计算一次。这类似“段到段”的保护。ECRC和LCRC虽然都是校验码,但保护范围完全不同。调试时如果你怀疑Switch内部或者某条链路有误码,可以对比总线上抓到的ECRC和LCRC是否正确,这也是协议分析仪一个很实用的功能。
需要注意的是,TLP Digest字段(ECRC)是Header中TD位为1时才存在。TD位如果置1,发送方必须在TLP末端附上4字节ECRC。很多软件驱动没必要用ECRC,所以TD位通常不置位,但这个机制在可靠性要求极高的场景(比如存储阵列、航空电子)里非常关键。
3. DLLP深度拆解
3.1 DLLP的作用范围与报文格式
数据链路层位于事务层和物理层之间,它面对的是一条真实的、可能有误码的物理链路。DLLP(Data Link Layer Packet)是数据链路层用于管理和维护这条链路的数据包。它不像TLP那样承载业务数据,而是负责确认TLP是否收到、通告Flow Control信用更新、管理链路电源状态等。
DLLP的格式比TLP简单得多。Header固定为3字节(其实是8比特的DLLP类型加16比特的附加信息),之后是16比特的CRC。总的DLLP长度远远小于TLP,这是因为它必须足够轻量,尽可能少地占用链路带宽。DLLP也不需要像TLP那样经过复杂的Flow Control流程,而是必须在发送缓冲区有空间时立即发送,这样才能及时反馈接收状态,避免出现死锁。
一个DLLP的典型构成是:DLLP Type(8比特)+ 具体类型相关字段(16比特)+ CRC(16比特),总共6字节。但DLLP实际在链路上出现时,还会被物理层包上一层帧标记(比如SDP、END),这是物理层为了做字节对齐加的。我们在协议分析仪中看到的DLLP,通常是以SDP开头、END结尾的一小段报文。
3.2 DLLP的常见类型
DLLP的类型不多,但非常关键。归纳一下常见的几种:
- ACK/NAK DLLP:这是数据链路层可靠性机制的核心。发送方每发一个TLP,都会在本地重放缓冲区保存副本。接收方正确收到TLP并校验LCRC通过后,回ACK DLLP;如果LCRC错误,则回NAK DLLP。发送方收到NAK或者超时未收到ACK,就会从重放缓冲区重新发送对应的TLP。
- 流控更新DLLP:前面说到Flow Control,接收方通过这类DLLP把当前各类信用余额告诉发送方,发送方根据这些数据来决定是否能继续发送TLP。常见的有InitFC1、InitFC2和UpdateFC。
- 电源管理DLLP:包括PM_Enter_L1、PM_Enter_L23、PM_Active_State_Request_L1等,用于协商链路进入低功耗状态。这个在移动平台的功耗优化里特别常见。
- 数据链路层状态相关DLLP:比如用于数据链路层状态初始化的DLLP,通常和链路两端的初始化握手绑定在一起。
从数量上看,一个高吞吐系统里最常见的DLLP是ACK/NAK和UpdateFC。如果你抓包发现连发几个TLP都没有ACK,而且对端重传频率很高,那条链路十有八九存在信号完整性问题或者CRC错误。
3.3 ACK/NAK重传机制的工作流程
链路两端的DLLP通过ACK/NAK机制实现可靠传输。发送方事务层每发一个TLP给数据链路层,数据链路层就会给它分配一个Sequence Number,并把它保存在一个重放缓冲区里。接收方数据链路层收到TLP后,先检查LCRC是否正确,正确就把该Sequence Number对应的TLP上交给事务层,并通过ACK/NACK DLLP把确认序号发给对端;如果LCRC错,就请求对端重发。
这里有一个很重要的细节:ACK/NACK确认的是“某个序号之前的所有TLP都收到了”,这个序号叫AckNak_Seq_Num。接收方不需要每收一个TLP都发ACK,它可以积累多个TLP后一次性确认,只要保证序号连续即可。这样做能显著减少DLLP数量,提升链路效率。
发送方一旦收到NAK或者连续重传计时器超时,就从头开始重发还没被确认的TLP。重放缓冲区大小是有限的,所以发送方在未确认数量达到缓冲区上限时,会被迫暂停发送。我们在做高吞吐DMA测试时,如果发现写入速率上不去,除了看中断和软件调度,还应该检查链路的重传统计是否偏高,因为重传会成倍地消耗链路带宽。
3.4 LCRC与数据完整性的边界
TLP端的完整性由LCRC保护,DLLP自己也有CRC保护,两者互相独立。收到一个带数据负载的TLP时,先由物理层恢复bit流,数据链路层识别出这是一个TLP,提取出整个TLP做LCRC校验。校验通过后再交给事务层。若校验失败,接收方不向上层递交,而是启动NAK流程请求重传。
LCRC的计算覆盖整个TLP,包括Header、Data以及TLP Digest(如果存在)。有些时候我们会在逻辑分析仪或协议分析仪上看到TLP本身的LCRC是对的,但TLP内部的数据载荷已经损坏。这种情况更常发生在端到端路径上,即发送方物理层发出后经过Switch或长走线时受到干扰,但每段Link的LCRC又被重新生成了——所以最终能被对端感知的数据损坏检测,很多时候要依赖ECRC。
4. 物理层数据包PLP与链路维护
4.1 PLP与物理层有序集
物理层是整个PCIe体系里离电气信号最近的一层。物理层的数据包被称作PLP(Physical Layer Packet),在实际链路上它并不像TLP/DLLP那样有明确的“包边界”概念,而是由一组组“有序集”(Ordered Set)来构成。常见的有TS1/TS2(Training Sequence)、SKP(Skip)有序集、EIEQS(Electrical Idle Exit Quiet Sequence)等。
TS1/TS2在链路训练时特别密集。PCIe设备上电或复位后,物理层会自动进入链路训练状态机(LTSSM),通过反复交换TS1/TS2有序集来进行位锁定、符号锁定、链路速率协商、通道翻转和通道聚合(Lane Reversal与Lane Bundling)等。如果TS1/TS2握手不成功,就会出现我们常见的链路反复训练、甚至直接Link Down的情况。
SKP有序集则用于消除两端时钟频偏带来的数据对齐偏移。每颗PCIe设备都有自己的本地参考时钟(Refclk)。即使两端标称都是100MHz,实际振荡器频率也有微小误差。物理层每隔一段时间就要插入(或删除)若干SKP符号来吸收时钟偏移,否则长时间传输后接收端的FIFO会被瞬间“挤爆”或“读空”。PCIe规范对不同速率的SKP插入规则有明确规定,这也是为什么在协议分析仪里面你总能看到大量的SKP有序集,它们不是噪声,而是维持链路稳定运行的“呼吸调节器”。
4.2 链路训练状态机与PLP的关系
链路训练是PCIe物理层最复杂的机制之一。从复位开始,物理层要依次经过Detect、Polling、Configuration、L0等多个状态。LTSSM(Link Training and Status State Machine)的每个状态切换,都伴随着特定PLP/有序集的收发。
- Detect:检测对端是否存在,主要通过接收检测电路探测是否有远端接收端电阻。
- Polling:发送TS1/TS2,进行位锁定和链路位宽协商,并交换端口能力。
- Configuration:确定通道反向、极性反转、速率协商等,最后进入L0。
- L0:正常工作态,可以收发TLP/DLLP/PLP。
- Recovery、L0s、L1、L2:用于错误恢复和低功耗管理。
调试开发板时常遇到的问题是:PCIe设备在系统中无法识别。用示波器探测差分对时,能看到周期性的TS1/TS2往返,但始终无法进入L0。这种情况多数是因为上游端口要求Gen3速率,而下游设备只支持Gen2,速率协商没成功;或者是某个Lane的极性反转没做对。此时去抓PLP层的TS1/TS2内容,就能看到双方各自声明的速率、链路宽度等信息,判断出问题出在谁身上。
4.3 弹性缓存与SKP补偿机制
前面提过SKP用于解决时钟频偏,这里值得展开。PCIe设备的接收端内部有一个弹性缓存(Elastic Buffer),它本质上是一个FIFO,写入时钟是恢复出来的时钟,读出时钟是本地参考时钟。由于发送端和接收端的参考时钟存在微小频偏,如果不做任何处理,每隔一段时间写指针就会比读指针快一点或慢一点。当偏差超过FIFO容量时,数据就会错位或者丢失。
SKP的作用就是让物理层能在不打断正常数据流的前提下,对弹性缓冲区的读写位置进行微调。具体实现是:发送端每隔固定数量的符号就插入一定数量的SKP符号,接收端在检测到SKP后,根据自身缓冲区的占用情况决定保留、删除还是增加SKP数量。这样一个“有损”的微调过程让收发双方能长期保持同步,又不会破坏有效数据。
调试时如果你在跑PCIe的BER测试或长时间大流量压力测试,关注SKP的插入频率和弹性缓冲区溢出状态是很有必要的。一些FPGA实现的PCIe硬核都提供了相关状态寄存器,通过它们可以判断时钟源的质量是不是达标。
5. 实操视角:用协议分析仪抓包解析三种包
5.1 从抓包文件中识别TLP/DLLP/PLP
纸上谈兵说得再多,都不如亲手抓一次包来得实在。PCIe协议分析仪(比如Teledyne LeCroy、Keysight、SerialTek的产品)会把链路上的物理信号还原成三类报文,并在界面上用不同颜色和标签区分TLP、DLLP、PLP。你可以在Trace窗口里看到类似下面的输出片段:
TLP: MemRd64, Tag 0x1A, Length 0x40, First DW BE 0xF, Last DW BE 0xF, Address 0xFE000000, Requester ID 0x01:00.0 DLLP: Ack, Seq_Num=0x105 DLLP: UpdateFC, VC0, Posted Header Credit=65520 PLP: SKP Ordered Set这类信息对快速定位问题非常有帮助。例如你做一次DMA读,发起端发出MemRd64 TLP后,如果长时间没有收到带数据的Completion,就可以检查中间是否存在UR(Unsupported Request)类型的Completion返回,或者TLP是否在链路层反复重传。每次数据吞吐上不去,我都建议先把DLLP的ACK/NAK和重传统计拉出来看,这一眼就能判断是不是链路误码导致的重传风暴。
5.2 关键报文字段的手动解析示例
使用协议分析仪时,我们经常需要手动查看原始报文十六进制来确认某些字段。这里我给出一个Memory Read TLP的简单解析例子(假设4字节Header,无数据负载):
假设抓到这样一段TLP原始数据(去掉链路上的帧头帧尾和LCRC):
00 00 1A 40 00 01 00 00 FE 00 00 00逐字节解读:
- 第1字节 0x00:Fmt=00,Type=00000,表示无数据负载的Memory Read请求(对于Memory Read,Fmt[2:1]不同配置代表32/64位和无/有数据负载)。
- 第2字节 0x00:TC字段为0,后续若干bit为Attr等属性。
- 第3字节 0x1A:Tag=0x1A,这个标签用于匹配对应的完成包。
- 第4字节 0x40:Length=0x40个双字,即256字节的读长度(64DW)。
- 第5-6字节 0x00 0x01:Requester ID = Bus 0x00,Device 0x01,Function 0x0。
- 第7字节 0x00:Last DW Byte Enable和First DW Byte Enable,全F表示4字节都有效。
- 第8-11字节:地址0xFE000000(64位格式的高低位)。
如果你在某个Read TLP之后看到这样一串Completion:
4A 00 1A 00 00 01 00 00 ...这里Type变成0b01010等等,其中包含了Completion状态、Completer ID、Requester ID、Tag和传输数据长度等信息。通过比对Completion里的Requester ID和Tag,就能确认它响应的是哪一个MemRd请求。这就是为什么我强调在做长时间DMA测试时,不要只听驱动日志说“传输完成”,建议在PCIe分析仪上同时核对TLP层的Tag匹配率,一旦发现某个Tag发出的读请求迟迟没有对应Completion,基本可以断定是设备侧逻辑或驱动超时有bug。
5.3 一个常见抓包场景:PCIe枚举过程
另外一个很适合新手练习抓包的场景是设备枚举(Enumeration)。系统上电后,Root Complex会按ID路由发起一系列Configuration Read/Write TLP。你在协议分析仪上会看到大量以Type=ConfigRd0/ConfigWr0开头的TLP,目标地址由总线号、设备号、功能号组成。枚举过程大致是这样的:
- Root Complex首先扫描Bus 0上的各个Device,向Device 0的Function 0发起ConfigRd,读取Vendor ID和Device ID。
- 如果没有设备存在,链路会返回一个“全F”的数据,软件据此判断该设备槽位为空。
- 找到设备后,软件继续读取Header Type、BAR寄存器等,并给设备分配总线号和资源。
- 如果遇到PCIe Switch,枚举过程会递归到下游总线,为每一个下游端口找到的设备分配独立总线号。
这一串枚举在协议分析仪上看起来非常简单,就是连续不断的ConfigRd/ConfigWr TLP和随之而来的Completion。如果设备不能被正确识别,直接在抓包Trace里搜索目标Bus/Device号,看是否存在UR状态的Completion,或者TLP是否根本没有发到设备端,能非常高效地定位问题。
6. 常见问题与排查技巧
6.1 如何区分链路问题是物理层还是事务层
实际调板时,最怕遇到“设备识别不到”这类问题,因为它可能发生在任何一个层。我的排查顺序是:先看物理层是否L0,再看得链路层是否正常握手,最后看事务层是否有TLP交互。
如果链路一直停留在Polling或Configuration状态,优先怀疑物理层信号质量:差分对是否接反、极性是否翻转、参考时钟是否稳定、链路速率协商是否一致。如果链路已经进入L0但系统里设备还是不可用,就要检查数据链路层是否完成了DLLP初始化,比如InitFC1/InitFC2交换是否完成。最后才看事务层设备返回的Completion Status。
这里分享一个我调试时常用的核对表:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 链路反复训练、无法L0 | 信号质量问题/速率协商失败 | 用示波器看眼图,抓TS1/TS2确认协商速率 |
| L0正常但TLP发不出 | VC credit不足 | 抓UpdateFC DLLP,检查信用通告 |
| 连续重传、吞吐很低 | LCRC错误多 | 抓ACK/NAK统计,检查信号完整性 |
| 枚举时读配置空间返回全F | 设备未上电/链路未就绪/设备不支持 | 抓ConfigRd和Completion,确认UR或超时原因 |
| DMA写入数据错误 | 端到端数据损坏 | 检查ECRC、LCRC、系统内存和BAR地址范围 |
6.2 TLP格式理解避坑指南
关于TLP格式,初学者容易踩几个坑。第一个是Fmt和Type的组合不是简简单单的查表,同样一个Type字段,不同的Fmt会得到完全不同的包类型,例如Memory Read有32位和64位之分,而且有数据负载时和无数据负载时格式还不一样。第二个坑是Header长度:带数据的写请求在Memory Write的Header是12或16字节,而Completion的Header是12字节,但Completion with Data实际上是“Header + Data”,要注意理解这里“带数据”的含义。第三个坑是Byte Enable和Length计算的配合:Length字段是双字数,而Byte Enable决定的是每个双字中哪几个字节真正有效,如果地址不是对齐的,翻车概率极高。还有Tag复用的问题,事务层必须保证同一时刻不会有两个相同Tag的Non-Posted请求在链路上“悬空”,否则收到完成包就无法区分归属。
我建议学习阶段拿协议分析仪的原始报文逐字节对照Spec练习解析,做过十几个样本后,再看这些字段就能形成条件反射了。
6.3 关于DLLP常见误区的几点澄清
DLLP虽然简单,但有几点特别容易混淆。其一,ACK/NAK是针对TLP的确认,不是对DLLP本身的确认。DLLP自己不带Sequence Number,也没有重发机制;如果接收端CRC校验失败,直接丢弃即可,因为DLLP是“当前状态”的表示,丢了后面还会发新的,不需要补偿。其二,流控更新DLLP的信用值不是“网速”或“带宽”,而是缓冲区剩余空间,所以数值变化是离散的、取决于收发双方缓冲区的消耗速度。其三,NAK并不能保证丢包一定被发现,如果某个TLP的LCRC错误,但接收方连续收到几个错误包,它只会发一个NAK,这时发送方把所有未确认包重发,是一种低效但可靠的策略。理解这些,读实现代码或者调试tips时会更顺畅。
6.4 FPGA/XDMA开发中与三种包相关的实际经验
如果你用Xilinx的XDMA或Altera的DMA IP做PCIe开发,你会发现IP核已经把TLP/DLLP/PLP的处理封装起来了,日常主要面对的是AXI接口和寄存器。但想排查性能问题,还是得了解背后的包处理逻辑。我有一次做XDMA连续写入测试,发现带宽总上不了线。后来在PCIe分析仪上看到UpdateFC DLLP特别频繁,而且Posted Header的Credit经常接近满值,才意识到不是链路瓶颈,而是对端接收Buffers太小,导致发送方不敢流水线式地发太多TLP。后来调整了XDMA IP核里的接收Buffer大小和Flow Control Credit设置,吞吐才恢复正常。
这种问题如果你不懂DLLP和Flow Control,会误判成“PCIe链路不稳定”甚至“IP核有问题”。这也是我反复强调要理解TLP/DLLP/PLP的原因——它们不是考试知识点,而是你排查问题的工具箱。做FPGA方案、驱动开发、板卡调试,遇到链路相关疑难杂症,最终都要回到这三层协议里去求助。
7. 后续学习路径与经验小结
这篇笔记围绕TLP、DLLP、PLP做了一次整体梳理。TLP是业务核心,理解它的格式、类型、路由和流控,是做驱动或FPGA设计的基础;DLLP是可靠传输的枢纽,ACK/NAK、重传、流控更新,都直接决定系统能跑多快、多稳;PLP则负责物理链路的训练和维护,很多“找不到设备”“链路不稳定”的故障最后都追溯到这一层。
建议下一步可以继续深入的方向:一是对照Spec逐条看TLP类型的字段布局,二是拿真实抓包Trace练习解析,三是结合LTSSM状态切换来理解PLP具体行为。尤其是Trace解析,能把前面所有抽象的协议概念落到具体报文上,效果远超读十遍书。
按照个人经验,这套学习笔记通常还需要两三篇才能把事务层细节、链路训练状态机、以及PCIe枚举和配置空间完整串起来。到时候我会继续把实际调试案例和抓包分析放到一起写出来,方便读者对照。