有次帮人排查一张PCIe接口卡在主机上识别正常、一跑批量读写就中途卡死的故障抓包软件一开满屏都是TLP可问题恰恰不是出在TLP上。翻了几万帧我才注意到链路两端的DLLP交互早就不正常了——ACK发得越来越少NAK风暴倒是很勤快。从那次之后我再没小看过这些只有5字节的小包。这篇文章就围绕PCIe-3.5这套叫法展开聚焦Data Link Layer PacketsDLLP。很多人习惯把PCIe 3.x中后期的小版本修订统称为PCIe-3.5其实DLLP这套机制在3.0之后没有结构性变化。无论你是做FPGA原型验证、自研PCIe控制器还是纯粹被链路问题折磨的驱动工程师搞懂DLLP你就能在链路出问题的时候多一个可靠的切入角度。1. 先别急着追TLPDLLP才是链路能跑起来的隐形骨架1.1 三层协议栈里DLLP到底属于哪一层PCIe协议栈分三层事务层、数据链路层和物理层。事务层负责生成TLPTransaction Layer Packet也就是我们最熟悉的那些内存读写、配置请求和完成包数据链路层则负责在两边的物理链路上可靠地搬运这些TLP同时管理链路本身的状态物理层则处理电气信号、编解码和链路训练。DLLP就是数据链路层自己的“工作语言”。它由发送端的数据链路层产生被接收端的数据链路层消费全程不进入事务层。换句话说软件、驱动、寄存器都看不到DLLP只有逻辑分析仪或协议分析仪能帮你把它们“翻译”出来。这一点很关键——很多人排查PCIe问题只盯TLP觉得TLP才代表真正的业务结果忽略了链路的可靠性、流控和电源管理全要靠DLLP在底下撑着。我用一个物流的类比帮新手建立直觉TLP是货车运的货物DLLP就是物流公司内部的对账单据、入库通知和车辆调度指令。货物很重要但没有这些内部单据仓库根本不敢让车进进出出车在哪个环节出了问题也没人知道。1.2 DLLP与TLP的分工谁管业务谁管链路把DLLP和TLP放在一起对比区别非常明显对比项TLPDLLP产生的层事务层数据链路层内容业务数据、地址、请求/完成信息链路控制信息长度3~4DW头可能带数据负载至少12字节固定5字节软件可见性驱动能感知完全不可见是否参与重传是由DLLP确认重传否DLLP出错直接丢弃主要作用完成端到端事务维护相邻设备间的可靠性DLLP实际承担的任务可以归纳为三类第一是TLP的确认与重传控制就是ACK/NAK第二是流量控制更新即Management和InitFC、UpdateFC第三是电源管理相关的握手比如进入低功耗状态的各种PM类DLLP。此外还有厂商自定义DLLP用于特定扩展。理解了这三种职责你再看链路数据就会发现TLP之间密密麻麻的DLLP并不是噪声而是保证TLP能够按序、不丢、不冲撞地传输的“交通警察”。没有DLLPTLP发出去就跟断了线的风筝一样发端不知道对方收没收到收端也不知道该按什么顺序处理。1.3 为什么链路管理功能不并进TLP里有人可能会问确认信息、流控信息为什么不做成一种特殊TLP非要单独搞一套DLLP我的理解是TLP是变长的而链路的控制信息需要尽量短小、固定、易处理。如果把确认逻辑做成TLP意味着接收端要把这个“确认包”送到事务层去解析事务层还得判断这是数据还是控制效率太低而且死锁风险会明显上升。DLLP固定5字节优先级高发送时机灵活可以插在TLP之间的空隙里。物理层也不会对它做复杂的地址解码和事务排序收端拿到后直接扔给链路层处理。另外DLLP设计成与TLP互不嵌套意味着事务层在忙业务的时候数据链路层可以独立完成自身的可靠性维护两边互不阻塞。这个解耦设计在PCIe这种超高带宽总线上非常关键。你试想一下如果每次TLP确认都要走一遍事务层的排队链路的有效带宽会被确认开销吃掉一大截。2. 5个字节的DLLP格式拆开看信息量不小2.1 固定长度与字段布局DLLP是固定5字节的这一点很多讲PCIe的书提得不多但实际抓包时特别好认。5字节具体拆解如下字节内容说明Byte 0DLLP Type8位说明这个DLLP的种类Byte 1~2类型相关信息16位具体含义完全取决于TypeByte 3~4CRC-1616位校验覆盖前3个字节CRC-16的生成多项式在规范里明确给定业界实现通常使用0x1021那种大家族的变体。它的覆盖范围是整个DLLP的前3个字节也就是Type加信息字段。接收端收到DLLP后先算CRC对不上就认为这个DLLP已经损坏。这里有个值得注意的细节DLLP出错之后不会像TLP那样靠对方回NAK来重传因为DLLP本身就没有重传机制。接收端直接丢弃它就完事了。那链路可靠性怎么保证答案是协议设计上做了兜底——比如因为DLLP丢失导致收端没及时回ACK发端可以靠重传定时器超时来触发重传因为UpdateFC丢失导致发送端停等则靠定时更新或者后续的UpdateFC来恢复。这就是为什么CRC校验在DLLP上虽然简单却足够关键它保证了“收到的DLLP一定是对的”而“没收到”由上层协议兜底。2.2 类型字段一眼看懂常见DLLP对照表DLLP类型值虽然多但规律性很强。我自己在生产环境里用的比较多的基本就是下面这张表Type值名称作用出现场景0x00ACK确认TLP收端校验TLP通过后回给发端0x01NAK请求重传TLP收端发现TLP序号异常或LCRC错误0x02PM_ENTER_L1请求/命令进入L1低功耗电源管理进入低功耗时0x03PM_ENTER_L23请求/命令进入L2/L3低功耗深度休眠流程0x04PM_ACTIVE_STATE_REQUEST_L1主动请求切换到L1ASPM机制0x08PM_REQUEST_ACK同意PM请求对端对PM请求的响应0x0BPM_REQUEST_NAK拒绝PM请求对端认为当前不适合进入低功耗0x10InitFC1-P初始化Posted信用链路刚进入L0时0x11UpdateFC-P更新Posted信用接收端释放了Posted缓冲0x12InitFC1-NP初始化Non-Posted信用链路刚进入L0时0x13UpdateFC-NP更新Non-Posted信用接收端释放了Non-Posted缓冲0x14InitFC1-Cpl初始化Completion信用链路刚进入L0时0x15UpdateFC-Cpl更新Completion信用接收端释放了Completion缓冲0x18/0x1A/0x1CInitFC2-P/NP/Cpl确认已完成FC初始化对方收到InitFC1后的回应0x20Vendor-specific厂商自定义功能具体看厂商定义通过这张表你再回看抓包数据通道里的DLLP节奏其实很有章法链路刚训练完先来一轮InitFC1Cpl等三个方向的初始化对端回InitFC2然后正常业务展开ACK、UpdateFC交替出现要睡觉了就来PM相关的DLLP握手。2.3 物理层上的模样怎么区分DLLP和TLP在物理层DLLP和TLP用不同的“帧起始符号”区分。TLP以STPStart of TLP符号开头DLLP以SDPStart of DLLP符号开头有序集则用其他控制符号。也就是说逻辑分析仪抓到的原始字节流里看到SDP后面跟5个字节就是一帧DLLP看到STP后面跟一长串数据就是TLP。初学者常犯的错是拿TLP的DW对齐思路去套DLLP。DLLP不按32位对齐逻辑上它就是5字节的小帧物理层会在它前后插入必要的符号填充。所以抓包软件上通常能看到协议分析器已经自动把DLLP识别成独立的一行而不是混在TLP的padding里。如果你在用裸的逻辑分析仪自己解析记住DLLP一定是SDP开头、5字节、CRC校验尾这个特征非常明显基本不会认错。3. ACK/NAK与重传DLLP给TLP上的安全绳3.1 12位序列号与重传缓冲Replay Buffer在数据链路层发送端会给每个TLP分配一个12位的序列号Sequence Number取值空间为0到4095。发送出去的TLP副本要留在重传缓冲Replay Buffer里直到收到确认之后才能释放。接收端则负责检查序列号的连续性和TLP本身的CRC。这里要理解一个核心思想DLLP不携带用户数据但它用ACK/NAK给TLP的可靠性做了“记账”。每个TLP是什么顺序发的发送端心里有数接收端有没有收全也是通过ACK/NAK告诉对端的。这个机制和TCP的滑动窗口确认有几分相似只是PCIe做得更简单直接。序列号的回绕从4095回到0是很多自研控制器容易漏测的地方。我建议验证阶段做一次极限压力测试让序列号连续跨越回绕边界确认ACK/NAK在回绕点时不会误判大小关系。这个测试写起来不难但不少方案都卡在这上面。3.2 ACK/NAK怎么判、怎么回接收端收到一个TLP后主要做两件事先看LCRC对不对再看序列号是不是预期中的下一个。两关都过了回一个ACK任何一关没过回一个NAK。ACK/NAK DLLP的信息字节里存放的是序列号信息。ACK告诉发送端“序号X以及X之前的所有TLP我都收到了你可以释放对应缓冲”NAK则告诉发送端“请从这个序列号开始重新发送”。注意NAK不代表“你发错了”它只是“我需要你把跟这个序列号有关的内容再给我一次”。累进确认的好处是接收端连续收到多个TLP时不需要为每个TLP单独回一个ACK可以把ACK合并成一条节省DLLP开销。实际抓包里你会看到正常链路的ACK频率并不高通常接收端处理后端的确认会攒一批再回。如果看到ACK和TLP的比例接近1比1甚至出现大量NAK那链路肯定出问题了。那种情况下别急着怀疑协议栈先想想信号完整性或者时钟抖动。3.3 重传定时器与NAK风暴实战还有一种情况TLP发出去了接收端既没回ACK也没回NAK比如TLP在链路上被噪声打得面目全非物理层根本识别不出一帧完整的TLP。这时代理能做的只有等——发送端有一个重传定时器一旦超时且确认没到位就假定对方可能什么都没收到直接从重传缓冲里把所有未确认的TLP重新发一遍。我在实际调试中遇到过一起典型的NAK风暴。某FPGA平台通过PCIe做DMA读带宽只有设计值的三分之一抓包看到链路上密集出现NAK紧接着整块整块的TLP重传。起初我怀疑是数据链路层的状态机写错了后来把分析仪探针位置挪近发现高速信号在连接器处有严重的反射LCRC错误率居高不下。接收端频繁校验失败自然不断回NAK发送端不断重传带宽全被重传吞掉了。换掉连接器、调整眼图参数之后NAK降到个位数问题立刻消失。那次之后我养成一个习惯看到任何异常的重传行为先查物理层再查逻辑层。DLLP的ACK/NAK逻辑本身并不复杂它只是忠实地反映了链路质量。链路差NAK风暴就是最终表现。4. Credit更新UpdateFC流量控制是DLLP里的银行账单4.1 基于信用的流量控制不是停等是授信PCIe的流控机制是典型的“信用制”接收端告诉发送端自己有多少接收缓冲发送端每发一个TLP就消耗掉对应的信用接收端处理完数据释放缓冲再用UpdateFC DLLP把新的信用额度补上。发送端只有在确信自己还有足够信用时才会发送对应类型的TLP。这三个类型分别是PostedP、Non-PostedNP和CompletionCpl。每个类型的信用又分成Header信用和Data信用两个维度。用银行授信来类比接收端是你办信用卡的银行信用额度就是FC Credit你每消费一笔发一个TLP可用额度就少一点还了款接收端腾出缓冲银行发UpdateFC告诉你额度回来了。这个机制极其优雅它抛弃了停等协议的低效让发送端可以连续发包同时保证不会把接收端的缓冲打爆。同样重要的是它天然区分了不同先行事务避免了Posted请求把Completion通路饿死的情况。4.2 InitFC与UpdateFC的配合节奏链路训练完成、进入L0状态后数据链路层会立刻做FC初始化。发起方发送TLP的一端先发InitFC1把自己能接收的初始信用公告出来对端收到后回InitFC2表示两边都完成了初始化。PCIe规定这个初始化分阶段还有InitFC2来确认初始化结束防止双方还没对齐就开工。实际操作中InitFC阶段最容易遇到的问题就是“链路已L0但网卡不发任何TLP”。这通常是因为某一边的FC初始化没走完或者初始信用通告错误。我的排查第一步永远是看抓包里有没有完整的InitFC1-P/NP/Cpl和对应的InitFC2-P/NP/Cpl少任何一个链路都无法正常跑业务。进入正常阶段后接收端只要有缓冲释放就发对应的UpdateFC DLLP。发送端据此更新自己的可用信用计数器。如果接收端一直不发UpdateFC发送端的信用慢慢耗尽就会停在该发送的门口表现为带宽上不去甚至完全卡死。4.3 FC DLLP字段细节与排障经验FC类DLLP的信息两个字节布局很有意思Byte 1的高4位是Virtual Channel IDVC号Byte 1的低4位加上Byte 2的8位共同构成一个12位的信用上限值Credit Limit。因为这个设计一个UpdateFC DLLP一次只更新一个VC的一个类型的信用需要更新多个信用维度时就得多发几个UpdateFC。这里还要强调一个实战坑FC Credit的单位不是字节。一个credit对应多少个DW取决于接收端的缓冲配置和双方协商的结果默认情况下和接收端缓冲的粒度有关但绝不能想当然地把credit数和字节数直接画等号。我见过有人把UpdateFC里的credit limit字段误当成剩余的字节数来用结果流量控制判断严重错误差点在验证板上把上层的DMA状态机搞崩。排障时还有一个很实用的判断当某个方向完全停住时用分析仪看FC DLLP的credit limit。如果limit长期不更新说明接收端没有释放缓冲问题在下游逻辑没消费数据如果limit在更新但发送端还是不发说明发送端的信用计算或状态机有问题。用这个二分法能迅速把故障定位到发送还是接收。5. 电源管理DLLP低功耗握手也走这一层5.1 PM类DLLP的类型与含义链路不会永远满负荷跑为了省电PCIe设计了从L0到L0s、L1、L2/L3等多级低功耗状态。这些状态迁移涉及链路层和物理层的联动而握手动作就是通过PM类DLLP完成的。常见PM类DLLP大概有这么几个PM_ENTER_L1表示进入L1状态PM_ENTER_L23表示进入L2/L3状态PM_ACTIVE_STATE_REQUEST_L1用于ASPM机制下的L1请求对端接到请求后用PM_REQUEST_ACK表示同意用PM_REQUEST_NAK表示拒绝。这套握手保证了低功耗迁移不是某一方“单方面宣布”就行的必须双方确认避免数据还在路上时某一端先断电。PCIe 3.x时代链路速率高功耗问题愈发敏感PM DLLP的使用频率也随之增加。很多服务器环境里ASPM策略稍微配置激进一点你就能在抓包里看到密密麻麻的PM_ACTIVE_STATE_REQUEST_L1和PM_REQUEST_ACK。5.2 一次L1进入握手的过程L1入口的握手流程大致是这样首先某一端判断链路空闲且允许进入低功耗就发送PM_ACTIVE_STATE_REQUEST_L1给对端对端如果认为当前没有未完成的业务回一个PM_REQUEST_ACK两边一起降功耗如果对端还有缓冲中的数据要处理或正在等待某个TLP就会回PM_REQUEST_NAK发起方收到拒绝后继续保持L0留待下次再试。这个握手走的是DLLP而不是物理层的某个有序集原因在于DLLP有CRC、有固定的协议语义能实现可靠的双向确认。如果只用物理层的电信号或简单命令字很难保证双方对状态迁移的理解完全一致。实际调低功耗时我最头疼的问题是“进入L1很积极唤醒却很慢”。抓包一看PM_REQUEST_NAK频发对端总是拒绝进入L1。这种事情多数不是协议的问题而是设备里有软件或硬件在持续产生TLP让链路一直“看起来有业务”对端自然拒绝睡。这种情况下先查业务再查电源策略别一上来就改PM DLLP的实现。5.3 和LTSSM的界线什么时候能看到PM DLLP初学者经常把物理层的链路训练状态机LTSSM和数据链路层的DLLP混在一起。记住一个铁律DLLP只在L0及其相关状态下发送在链路训练阶段比如Configuration、Recovery你永远看不到DLLP看到的只有TS1/TS2有序集。PM类DLLP也遵守这个规则。它是在L0态下交换的“请求”请求成功后两边链路通过LTSSM配合往L1或更低状态迁移。一旦链路真的进入了低功耗状态物理层上不再有正常的DLLP传输直到唤醒后重新回到L0。所以抓包时如果你想看PM握手要把分析仪触发放在进入低功耗之前的那一段而不是等链路完全静默之后。这个界线理解清楚能避免一个很蠢的调试行为在链路训练期间眼巴巴等DLLP等不到就怀疑设备没跑起来。设备没问题是观察时机选错了。6. 抓包怎么看DLLP一套排查链路问题的实战方法6.1 分清三类东西Ordered Set、TLP、DLLP用协议分析仪看PCIe首先要分清楚总线上跑的三类数据结构。有序集Ordered Set是物理层的东西比如TS1、TS2、SKP、FTS负责链路训练、时钟补偿和速度协商等TLP是事务层的承载业务DLLP是数据链路层的就是我们这篇文章的主角。区分方法很简单看起始符号。STP开头是TLPSDP开头是DLLP而有序集有自己特定的控制符号。在分析仪的协议解码视图里协议分析器会帮你把三类数据结构用不同的颜色或图标区分开。如果你用的是裸的逻辑分析仪按前面说的特征自己解析也不难。我见过不少工程师盯着TLP的请求延迟分析性能却忽略了DLLP的占比。其实当DLLP占用带宽过高时TLP的有效带宽一定会下降。比如ACK过于频繁、UpdateFC过于密集都会挤占链路带宽。看到有效带宽不符合预期先看看DLLP是不是“话太多”。6.2 按阶段盯关键DLLP链路行为是分阶段的每个阶段要有不同的关注点阶段重点观察常见异常链路训练完成、刚进L0InitFC1-P/NP/Cpl、InitFC2-P/NP/CplFC初始化缺失链路处于“空L0”不发数据正常业务阶段ACK/NAK比例、UpdateFC节奏NAK风暴、ACK比例异常升高、信用长期不更新低功耗阶段PM_REQUEST_ACK/NAK频率频繁拒绝进入低功耗、唤醒后链路挂起长时间稳定运行CRC错误、重传事件DLLP CRC错误率上升链路信号质量劣化这个表是我自己的固件调试checklist演化来的。每次拿到新的平台先把这些阶段的DLLP基线抓一遍存成日志后面再出问题就有参照物了。6.3 我踩过几次坑之后沉淀的几条DLLP排查经验最后分享几条真正实战后沉淀下来的经验。第一遇到链路性能不稳别只盯着第一个出错的TLP往回翻几十微秒看看ACK和UpdateFC有没有出现连续的延迟或丢失。DLLP的异常往往比TLP异常早几百甚至上千个时钟周期是你定位根因的“前哨”。第二判断“对方没发”还是“我方没额度”时直接看FC DLLP。如果链路空转但发送端迟迟不发TLP十有八九是某一类信用降到了零。看到credit limit在涨说明接收端在释放缓冲问题可能在发送端的信用跟踪逻辑看到credit limit不涨说明下游消费逻辑卡住了。第三DLLP的CRC错误是很好的物理层健康风向标。DLLP短小很多偶发CRC错误不会立刻表现为NAK但一旦DLLP CRC错误率开始上升哪怕只是每小时几次也要认真查信号完整性。它通常比同链路的TLP LCRC错误更早暴露问题。等到NAK风暴出现再处理往往已经是“血压爆表”的阶段了。第四也是最重要的一点所有DLLP测试都要覆盖序列号回绕、信用计满回绕和PM请求的拒绝路径。这三个边界是我在多个项目里反复遇到bug的地方。正常路径跑通不代表边界没问题PCIe链路一旦遇到边界状况反应往往比较剧烈而DLLP正是这些边界逻辑的载体。把这几条边界用例压一遍DLLP这块就基本稳了。
阅读完成 · 觉得有帮助?