1. 先从几个容易混淆的问题说起1.1 PCIe到底是个什么“协议”做底层开发这些年我最常被问的一句话就是“PCIe到底是个协议还是一条总线”实际上PCIe的全称是Peripheral Component Interconnect Express官方定位是“高速串行点对点互连协议”。它由PCI-SIG组织维护用来替代老掉牙的PCI/PCI-X并行共享总线。和以太网、USB这类协议一样PCIe并不是一张简单的接口定义表而是一套非常完整的分层协议栈其中包含事务层、数据链路层、物理层三层架构。这也是这篇文章标题里“三层架构”的来历。但有个细节常被忽略PCIe既定义物理电气特性也定义链路管理机制还定义软件可访问的配置空间。也就是说从你掰开SSD看金手指到操作系统里用lspci看到BDF号中间涉及的所有环节都跑在这套三层协议上。学习PCIe协议不能只盯着某一个layer看否则后面调起问题来会特别痛苦。这篇内容适合刚接触PCIe的硬件、FPGA、嵌入式软件开发同学也适合那些已经能跑通Demo但一旦遇到链路不稳定、枚举失败、带宽不达标就不知从何下手的人。1.2 点对点、串行、差分这些词怎么理解要理解协议先要理解物理形态。PCIe是点对点串行总线每个PCIe链路Link只连接两个设备中间要扩展就加Switch。这和传统PCI的共享总线完全不同PCI设备挂在同一条并行总线上大家抢带宽PCIe每一个Endpoint独占自己的带宽哪怕旁边设备在疯狂收发数据也不影响你的链路速度。举个生活例子PCI像一条单车道乡道所有车挤在一起PCIe像每条连接都是单独一条高速公路只是中间通过立交桥Switch互通。串行和并行也不能按字面理解成“慢和快”。PCI那种并行总线靠很多根线同时传32位或64位数据速度提上去之后线间串扰、时钟偏斜、信号完整性全是灾难。PCIe改走串行一对差分信号线每次只传1bit但靠极高频率换来远高于并行总线的吞吐量。差分的意思是一条lane里发送端会同时给出一正一负两根信号接收端只看两根线的差值。哪边有共模噪声两边一起被抬差值不变抗干扰能力就强很多。实测下来同样速率下差分对的抖动容限比单端信号好得多这也是PCIe能做到几十GT/s的原因之一。物理上每个lane由两组差分对组成一组发送、一组接收合称一个lane。所以x1链路看起来至少4根信号线x4就是16根。很多新手看原理图只数数据线忘了还有参考时钟或者把发送接收搞反后面会提到。记住一个概念PCIe链路描述为“x几”时指的就是有几条收发差分通道并行工作带宽按倍数增长。2. 三层架构整体视图2.1 为什么要把协议拆成三层PCIe设计成三层架构不是发明者拍脑袋而是借鉴了网络协议的分层思想。事务层、数据链路层、物理层各管一摊层与层之间有清晰接口这样PCIe既可以跑在芯片内部也可以跑在铜缆、光模块上甚至未来改成新的物理介质上两层完全不用动。这种解耦对硬件IP复用和系统演进太重要了。你换一个Gen4 PHY事务层和驱动代码基本可以不动这就是分层厉害的地方。拿快递打比方事务层是“寄件人和收件人”只关心包裹内容和地址数据链路层是“快递公司分拣系统”负责给包裹贴单号、扫码、确保不丢不破物理层是“货车和公路”负责把包裹从一个城市运到另一个城市。上层不需要关心车胎是否爆了下层不需要知道信封里写的是什么。PCIe三层之间的关系也是这个道理。每一层都有自己的协议数据单元。事务层叫TLPTransaction Layer Packet数据链路层在TLP外面加Sequence Number和CRC之后仍叫TLP从链路层角度看是被封装的包同时链路本身还有自己的DLLPData Link Layer Packet。物理层则处理symbol、编码、并串转换。软件只看得见事务层提供的读写事务至于中间怎么重传、怎么流控软件都无感。这也是为什么很多时候驱动代码看起来并不复杂。2.2 三层之间怎么传话先看发送方向。软件发起一次内存读RC内部的事务层会生成一个Memory Read TLP把它交给数据链路层。数据链路层收到后给这个TLP添加一个Sequence Number再算一个LCRC附在尾部然后交给物理层。物理层做的事情是编码、扰码、并串转换把数据变成高速差分信号送到对端。对端物理层收到信号后解码、去扰码、串并转换把还原出的TLP带着序列号和LCRC交给数据链路层。数据链路层校验LCRC无误检查序列号连续性去掉序列号和LCRC再把原始TLP上交给事务层。这里特别容易搞混的是DLLP。很多人以为数据链路层就是在TLP外加头尾其实链路层自己也会主动发送DLLP比如ACK/NAK、流控更新、电源管理事件。DLLP不会上送到事务层它在两个链路段点之间的数据链路层就直接消费掉了。换句话说DLLP是链路层“内部通信用的信件”和TLP这种“业务包裹”是两个通道。物理层并不区分谁是TLP谁是DLLP它只按物理symbol流来收发。接收方向就是上面过程的严格逆过程。事务层必须收到一个完整无错的TLP才会上送如果LCRC校验失败链路层会通过发送NAK要求对方重放。实际操作中协议分析仪上看到的报文已经混合了TLP和DLLP要学会先按类型过滤否则很快就会被ACK/NAK淹没。2.3 一个数据包从CPU到设备要经过什么还是拿最常见的场景说CPU要从NVMe SSD读一段数据。软件调用MMIO或DMA描述符RC收到请求后生成Memory Read TLP。这个TLP从RC的事务层出发进入Root Port的链路层、物理层通过RC与Switch之间的PCIe链路到达Switch。Switch的物理层收到后把包逐层解封装数据链路层校验无误再根据TLP头部的路由信息查路由表确定该从哪个下游端口转发。转发时Switch的相应端口会重新走一遍数据链路层封装和物理层发送把这个TLP传到硬盘控制器Endpoint。硬盘控制器收到后事务层解析返回Completion TLP带上读到的数据沿途再走同样的流程回到RC。这样一次传包在三层架构里实际发生了至少两次完整的封装-解封装。这里面还有路由方式的区分。Memory和IO TLP按地址路由Configuration TLP按ID路由BDF号Message有隐式路由、地址路由、ID路由三种。初学者常被“地址路由”绕住内存地址是系统全局地址而设备BAR地址在枚举时被映射成内存地址所以Memory TLP能像普通内存访问一样直接送到目标设备。这个路由动作正是由设备ID和BAR映射共同决定搞懂它之后很多枚举和地址分配问题都能迎刃而解。3. 事务层协议的心智所在3.1 事务层在管什么通俗地说事务层就是CPU侧和设备侧通信时的“翻译官”。它把上层发出的读、写操作变成标准格式的TLP也把收到的TLP还原成读返回、写数据并交给上层。它管的范围包括事务类型、地址空间、TLP格式、请求与完成配对、顺序规则、Tag管理还有可选的虚拟通道和ECRC。其中“事务”这个词的意思是一次访问由请求和完成两部分组成。比如CPU读一个地址先发一个读请求然后等设备返回一个带数据的完成包期间CPU可以去干别的事这就是split transaction。PCI的老用户对split transaction可能不太适应。PCI是共享总线发起一个读操作后总线会被占用直到数据回来PCIe则完全解放了这种锁占用。设备写CPU内存也是一样发一个Posted写事务不用等任何完成写操作就被“甩出去”了。由于不用占用全局总线PCIe的并行度和效率一下子高了很多。代价是增加了复杂度协议要处理乱序、要管理Tag、要避免通道ID冲突。看到这里你应该明白事务层绝对不是简单拼包它承载了协议最核心的脑力活。从编程视角来看内存映射I/OMMIO就是事务层最熟悉的入口。CPU对设备BAR地址的read/write会被RC硬件翻译成相应的Memory TLP。驱动工程师通常不需要直接构造TLP但理解事务层对解释性能问题很有帮助。例如一次MMIO读的延迟实际上是一个Non-Posted请求加上一个完成包往返远远不止“读一次寄存器”那么简单。3.2 TLP长什么样TLP的总体格式是可选前缀Prefix 头部Header 数据Data 可选ECRC。头部通常有3个或4个双字DW每个DW是4字节。带64位地址的Memory请求使用4DW头带32位地址和大多数其他事务使用3DW头。头部里的关键字段有Fmt/Type格式和类型、TC流量类别、TD是否含ECRC、EP表示错误毒化、Attr属性、Length数据长度、Requester ID、Tag、地址等。对于完成包还会有Completer ID和Status字段。我建议初学者不要死记所有字段先把Fmt/Type、Length、Requester ID、Tag、地址这几个搞懂。比如Length字段的单位是DW不是字节。一个Memory Read请求的Length为1表示要读4字节如果读16字节Length就是4。如果你在FPGA里手动拼TLP很容易在这里把字节和DW弄混然后就是各种枚举失败或者读回数据错位。数据负载不是任何TLP都有的Memory Write、IO写、Completion有数据Memory Read、IO读、Configuration读没有数据只有请求头。TLP的规范里还有一个容易忽略的边界就是Max_Payload_SizeMPS。PCIe允许数据负载大小根据系统配置从128字节一直到512字节甚至更大。发送方不能超过MPS接收方也要能处理。跨设备转发时如果经过的链路MPS不一致PCIe规定不能拆分TLP必须由软件统一配置这在高性能存储场景里是个很常见的坑。你明明把EP的MPS配置成512但RC根端口MPS只有256数据超过256就可能直接报错或不完整。3.3 四种事务类型与地址空间PCIe的事务类型可以按地址空间分为四类Memory、IO、Configuration、Message。其中Memory、IO、Configuration都有读和写两种基本操作完成包也有完成读、完成写两类。Message是个很特殊的东西它没有目标地址用来传递边带信息比如电源管理事件PME、错误信令ERR、锁存通知Latch等。现代系统里IO空间基本只是为兼容PCI老设备而保留普通PCIe设备几乎全部走Memory空间。Configuration空间则是在枚举阶段由RC主动访问用来读取设备的Vendor ID、Device ID、Class Code配置BAR地址和中断信息。Message虽然不被普通软件直接使用但它的作用绝不能被低估比如设备要通知系统“我要请求电源管理状态切换”就是发一个PME MessageRC向所有设备广播复位则用Hot Reset Message。调试时如果看到奇怪的Message先别当成垃圾流量那很可能就是链路或电源管理在干活。用表格看得更清楚事务类型典型用途是否地址路由请求/完成Memory Read/Write访问设备BAR、DMA地址路由Read需要完成Write多走PostedIO Read/Write兼容PCI老设备地址路由Non-PostedConfiguration Read/Write枚举、配置BDF/BARID路由Non-PostedMessage中断、电源管理、错误隐式/地址/ID路由无完成这张表里的Posted和Non-Posted概念后面会再细说。现在先记住配置和IO事务都是Non-Posted必须有完成Memory Write和Message通常是Posted不需要完成Memory Read则是Non-Posted。另外中断机制已经从PCI的INTx引脚变成MSI/MSI-X的Memory Write事务很多工程师调完PCIe发现普通中断触发不了其实是没分清楚传统INTx和MSI的区别。3.4 请求与完成以及乱序完成需要注意的点请求与完成配对是事务层最容易出问题的环节。每个Non-Posted请求会分配一个Tag完成包必须带相同的Tag软件或RC才能把完成包和原始请求对上。Tag数量有限RC通常支持几十个到上百个Tag如果一个请求迟迟没有得到完成Tag一直占着不放最终会耗尽资源新的请求就发不出去。所以调试时如果发现大量请求挂起第一反应要看Tag池是不是爆了。PCIe允许完成包以任意顺序返回只要符合协议规定的ordering规则。也就是说你先后发出了两个读请求后一个的返回完成完全可能先到。硬件必须能按Tag识别并乱序上送但很多自研IP或FPGA原型验证到这里就开始出问题了。实际项目中我见过最典型的坑是自己搭的RC只实现了很少的Tag一次只能挂几个读请求带宽自然上不去等把Tag池加大又要面对乱序完成处理逻辑复杂度立刻上来。Posted/Non-Posted/Completion三类事务之间协议文档里有一张详细的ordering table规定哪类事务可以越过哪类事务。很多人觉得这是硬件的事软件不用管但一旦碰到驱动里读写寄存器之间有依赖或者DMA完成中断与数据可见性问题就需要回来翻这张表。经验之谈不要臆测顺序凡是网上查不到答案又怀疑乱序的场景直接用协议分析仪抓一下TLP顺序比抱着文档猜快得多。4. 数据链路层可靠传输的保证4.1 数据链路层核心职责如果说事务层是协议的大脑那数据链路层就是PCIe的“脊柱”。它的职责用一个词概括就是“可靠”。数据链路层要为两个直接相连的端口之间的TLP传输提供无差错交付服务方式包括给TLP加序列号Sequence Number和LCRC、利用ACK/NAK实现重放、管理端口之间的流控Flow Control、生成和解析DLLP、以及参与部分链路电源管理。数据链路层的状态机还会影响LTSSM但LTSSM本身属于物理层两者互有接口这点先记住。很多开发者第一次调PCIe驱动看到协议分析仪里有大量ACK/NAK以为出了问题。实际上链路正常工作时也有ACK。数据链路层在收到一个正确TLP后会通过DLLP给对方回一个ACK如果发现LCRC错误或序列号不连续就回NAK让对方重放这段缓冲的TLP。ACK/NAK机制保证了TLP在单段链路上的可靠传输但这个可靠性是链路级的不是端到端的。Switch会把每段链路独立对待上一段已经确认的TLP到了下一段可能还需要重新确认所以不能简单把PCIe当成“一传到底不掉包”。这里有个容易误解的点数据链路层保证的是“不丢包”和“错包重传”但并不是说上层完全不用关心错误。当TLP头里EPError Poisoned位置位时接收端虽然完成了链路层校验但仍会把带毒包上送事务层由软件或设备决定要不要报错。另外数据链路层的重放缓冲区大小是有限的如果链路质量差到重放频繁会导致性能急剧下降甚至触发生成重放超时错误。所以你在系统日志里看到类似“replay timeout”的报错基本可以断定是物理信号层出了问题而不是协议栈本身。4.2 DLLP、ACK/NAK 与重放机制DLLP是数据链路层的自有报文长度固定为6字节格式大致是DLLP类型、数据、CRC。它包含ACK/NAK、流控更新Update_FC、电源管理事件、初始化流控InitFC等类型。DLLP只在两个直连端口之间的数据链路层交换不会上送事务层也不会跨Switch转发。物理层在传输DLLP时同样要经过扰码和编码但它优先级往往高于普通TLP这样链路控制信息不会被后台大数据流量饿死。ACK/NAK的工作流程大概是发送端数据链路层维护一个重放缓冲区所有待确认的TLP都存在里面。接收端每收到一个TLP就检查LCRC和序列号。如果正确接收端更新期望序列号并回复ACK发送端收到ACK后就可以把对应及更早的TLP从重放缓冲区移出。如果接收端发现序列号不连续或者CRC错就回复NAK并要求从该序列号开始重放。因为PCIe是全双工链路ACK/NAK可以随时插入回传方向并不需要等请求号。协议分析仪里看起来“一包一ACK”但实际是批量处理的接收端可以多个TLP合并一个ACK也可以对某个序列号ACK表示“这个号之前的包我都收到了”。重放机制听起来简单但实现上要做细微的时序对齐因为ACK本身也可能丢失。PCIe允许发送端在超时后自动重放未确认的TLP并采用更保守的超时参数防止同时发送大量数据导致缓冲爆掉。在链路退化的情况下你看到的带宽掉得非常诡异其实往往就是重放开销在作祟。4.3 流控到底怎么控流控Flow Control是数据链路层里最容易和ACK/NAK搞混的功能也是很多工程师理解PCIe链路性能的门槛。先说结论ACK/NAK是接收端确认收到流控是发送端在发之前确认“你有地方放”。流控机制不是为了纠错而是为了避免发送端把接收端缓冲区灌满。PCIe并不是一个像UDP那样发出去不管的链路在事务层和数据链路层之间需要buffer空间。具体实现是信用Credit机制。每个端口初始化时通过InitFC1和InitFC2 DLLP告诉对端自己为六种TLP类型Posted Header/Data、Non-Posted Header/Data、Completion Header/Data各分配了多少个FC Credits。发送端发出一个TLP就要扣减对应类型的credit接收端使用完缓冲区并释放后通过Update_FC DLLP返还credits。credit耗尽之后发送端就不能再发对应类型的TLP只能等待更新。这个设计和TCP窗口有点神似但粒度更细。实际调试中流控可以解释很多奇怪现象。比如一个EP的Completion credits太少RC读到大量数据阻塞在发送端吞吐上不去。又比如Non-Posted credits耗尽读请求发不出去链路看起来是L0但load始终很低。处理这种问题可以在启动时读一下双方的FC Credits很多PCIe分析仪可以直接显示如果发现某个方向credit设得特别小多半是IP例化时的buffer配置问题不是驱动问题。重要是流控是逐跳机制RC和Switch之间、Switch和EP之间各有各的信用空间Switch内部要为此预留buffer这也是Switch贵的原因之一。5. 物理层比特到底怎么跑5.1 物理层的子层划分物理层再往下拆分主要分逻辑子层和电气子层。逻辑子层负责跟链路层对接、处理symbol、编码、扰码、弹性缓冲、多lane的pattern剥离和bonding电气子层负责差分驱动、接收均衡、阻抗匹配和时钟恢复。对于FPGA开发者最常接触的是PIPE接口这是Intel推出的一种PHY接口标准把物理层进一步抽象成MAC和PHY两部分。PIPE接口里有TxData、RxData、电源管理状态、速率协商控制等信号FPGA里例化PCIe IP时会清楚看到这条边界。回到协议基本概念物理层并不理解什么是TLP它只看到一串串逻辑symbol。TLP到数据链路层加上序列号和CRC后会被切分并转换成物理层规定的编码数据块。物理层还要维护链路训练状态机LTSSM这是PCIe物理层跟前几代并行总线最大的区别。并行总线一上电就开始传数据而PCIe必须由物理层先完成链路训练才能建立起稳定的收发参数然后上层事务才可能跑起来。很多人看到枚举失败第一反应是驱动、配置空间其实一半以上的问题都停在物理层没训练成功。5.2 编码方式与速率演进PCIe的各代速率演进对所有调过高速接口的人来说都是重点。大家常说的x1/x4/x8/x16只是路数单条lane的速率才是基础。第一代PCIe 1.0是2.5GT/s采用8b/10b编码第二代5GT/s依然用8b/10b到第三代8GT/s开始改用128b/130b编码编码开销大幅下降速率也从此跳到了8开头第四代16GT/s、第五代32GT/s第六代64GT/s也在路线上。注意GT/s是“每秒传输多少G个symbol/bit”不是“Gbps的有效数据速率”。换算有效带宽时一定要扣除编码开销和协议总开销。比如PCIe 3.0 x18GT/s乘以128/130得到约7.877Gbps再除以8约为0.9846GB/s四个lane就是约3.94GB/s。不少人拿8GT/s直接算得出来8GB/s那是错的。PCIe 4.0 x4理论约7.88GB/sPCIe 5.0 x4约15.75GB/s。实际带宽还会被TLP头、DLLP、ACK等协议开销吃掉一点所以跑分永远比理论峰值低几个百分点。代次每lane速率编码x4理论有效带宽PCIe 1.02.5 GT/s8b/10b~0.984 GB/sPCIe 2.05.0 GT/s8b/10b~1.969 GB/sPCIe 3.08.0 GT/s128b/130b~3.938 GB/sPCIe 4.016.0 GT/s128b/130b~7.877 GB/sPCIe 5.032.0 GT/s128b/130b~15.754 GB/s这里建议新手记住一个粗估经验PCIe 3.0以后单条lane的可用带宽大概为Gen3约1GB/s、Gen4约2GB/s、Gen5约4GB/s再乘lane数就是粗略的x几带宽。比如Gen4 x1约2GB/sGen4 x4约8GB/s。这个估算在方案选型阶段够用写技术文档报告时还是用精确值更严谨。5.3 Lane、Link 与差分对物理层一个lane就是一对TX差分线加一对RX差分线。Link是多个lane的组合协议规定支持x1、x2、x4、x8、x12、x16、x32实际常见的是x1、x4、x8、x16。链路训练时两端会协商出双方都支持的最大lane数。如果一边是x16一边只能x4最终会降到x4。这种宽度协商是在物理层Configuration状态完成的所以枚举还没开始链路宽度就已经定死了。多lane传输并不是简单地把8字节数据拆成每根线传1bit就完事。PCIe必须做lane-to-lane的de-skew因为每条lane的走线长度、过孔数量不可能完全一致到达接收端的时刻会有微小偏差。物理层使用弹性缓冲和补偿机制把这些偏差吸收掉保证所有lane上同一周期的数据仍然能拼回正确的字节序列。layout时虽然自动lane reversal和极性翻转能降低布线难度但每组lane之间的等长要求依然严格尤其是Gen4以上几mil的差异都能影响误码率。另外参考时钟也是物理层一大坑。PCIe支持共用参考时钟Common Refclk、独立参考时钟Independent Refclk和数据时钟恢复Data Clocked Rx几种架构。如果使用独立参考时钟两端频率允许在一定ppm内偏差靠接收端的CDR来跟踪。调试中常见链路训练一直失败测量REFCLK才发现频率偏差超了标称。还有AC耦合电容PCIe要求串在发送端的电容一般为75nF到200nF如果板上贴了普通退耦电容低频成分过不去link一样不稳定。这些细节不像软件那样看日志就能定位必须用示波器和眼图来查。5.4 LTSSM链路状态机简单说LTSSMLink Training and Status State Machine是物理层的核心状态机负责把链路从无电状态一步步带到L0正常工作状态。常见状态包括Detect、Polling、Configuration、L0、L0s、L1、L2、Recovery、Hot Reset等。上电后先Detect检测对端是否在线然后Polling发送训练序列Configuration阶段协商lane数和速率成功后就进入L0传输数据。后面L0s/L1是低功耗状态Recovery则用于速率重协商或链路退化后的恢复。调试中抓LTSSM是最有效的诊断手段之一。比如卡在Polling大概率是信号没上来或对端没有回应训练序列反复在Configuration进出通常是lane极性、lane reversal或速率协商出了问题已经到L0但一跑流量就掉回Recovery基本可以认定信号质量差触发了重协商。常见PCIe分析仪都能显示LTSSM状态但嵌入式环境里没分析仪时很多SoC的Debug寄存器也能读当前状态建议提前查手册关键时刻能救命。6. 从软件视角看三层架构6.1 配置空间与枚举PCIe的软件视角和传统PCI高度重合核心就是配置空间和枚举流程。每个PCIe设备/Function都有一个配置空间前256字节兼容PCI后面还有扩展配置空间总大小可达4KB。配置空间里最重要的几个寄存器是Vendor ID、Device ID、Class Code、Command/Status、BARBase Address Register、Capability Pointer以及PCIe扩展能力块比如AER、MSI/MSI-X、Link Control/Status等。系统上电后RC负责枚举所有设备。它先扫描总线0上的设备0功能0读到Vendor ID有效就继续配置否则跳过。遇到PCIe-PCIe桥Switch时RC会给它的下游总线分配一个Bus Number然后递归扫描。每个设备会被分配一个唯一的BDFBus:Device.Function号同时软件通过配置读写把设备的BAR size探测出来并为其分配系统内存地址。这个过程看起来复杂其实产生的TLP就是Configuration Read/Write请求一层层走三层架构最终到达目标设备。所以枚举失败时先从RC是否发出Type 0/Type1配置事务开始排查而不是一上来就怀疑驱动。由于PCIe继承了PCI的软件模型大多数操作系统里看到的PCIe设备依然是挂在一棵PCI拓扑树上。Linux下lspci -tv可以看树状结构Windows设备管理器也能按连接关系查看。值得多说一句BAR大小检测的方式是把全1写入BAR寄存器再读回看看哪些位可以被清零。这个行为必须由设备硬件正确实现如果EP的BAR实现不规范枚举时RC会读出错误的大小之后所有访问都会错位。做FPGA原型验证时这是最常见的自检项之一。6.2 RC先启动还是EP先启动热词里有个“pcie ep先启动还是rc先启动”这问题几乎每个项目都会被问一次。实际答案是PCIe链路的建立不依赖RC或EP谁先启动软件它只要求两端物理层准备好。复位释放后LTSSM就开始自动训练不需要BIOS或驱动参与。但这里的“准备好”包括供电稳定、参考时钟稳定、PERST#已释放。如果一个EP需要加载固件才能进入可训练状态设计上必须让固件加载先完成或者等PERST#释放后再开始训练。如果你发现EP上电后固件还没起来RC已经开始高频扫总线链路状态自然在Detect/Polling之间反复横跳。系统里常见的做法是用PERST#来控制这个先后顺序。PERST#可以看作对整个PCIe子系统或一个端口/设备的硬件复位它由RC侧或系统管理控制器统一管理。对于需要加载固件的EP固件加载完成后才释放该设备的PERST#对于RC它自己的参考时钟和复位都来自平台等RC侧硬件稳定后它再释放下游设备的PERST#。所以严格的顺序是先保证两边都有稳定电源和时钟再按设计释放PERST#最后等LTSSM进入L0。软件枚举是在链路已经是L0之后才会发生。如果RC和EP的复位逻辑完全独立没有统一控制就可能出现RC枚举时EP还没ready的情况。这时候RC读到配置空间全部为0xFF会认为这个槽位没有设备。解决办法通常是利用热插拔或链路状态变化事件等EP就绪后再次触发枚举。我在一个多FPGA加速卡项目中就踩过这个坑三张卡上电时间本来就不一致RC一次性扫不到某些卡后来改成每张卡独立PERST#控制按从卡到主卡的顺序释放问题才消失。6.3 调PCIe时最常踩的三个坑聊了这么多基本概念最后把实战里最常踩的坑翻出来希望能帮大家少走弯路。第一个坑是只看LTSSM到了L0就以为万事大吉。L0只表示链路训练完成、可以传输TLP不代表信号质量就好。链路工作在某个Gen下但误码率高到不停重传软件层看吞吐就是上不去日志里可能只有零星的报错。正确做法是在L0状态下跑压力测试同时看物理层的BER、眼图margin、Lane Error Counter等指标。买一个支持协议统计的PCIe分析仪会省很多时间。第二个坑是不管REFCLK和AC耦合电容。参考时钟的ppm超差、AC电容容值不对、layout上REFCLK走线太长都会导致LTSSM反复训练不成功。尤其是复用通用开发板的时候看起来PCIe金手指都连上了实际是几个高速AC电容焊错位置或者容值不对。遇到link不稳用量出来的REFCLK波形做第一判断比反复改驱动参数有效得多。第三个坑是抓包时机不对。不少人一上来就抓TLP发现协议分析仪里全是ACK/NAK就误以为协议模块坏了。实际上应该先确认物理层状态机和数据链路层状态再过滤TLP。PCIe是分层协议一层一层的看错误一定藏在某一层的边界上。只要能区分TLP、DLLP和物理层symbol90%的问题都能快速缩小范围。
