ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

PCIe基础详解:从系统拓扑到带宽计算与硬件设计要点

PCIe基础详解:从系统拓扑到带宽计算与硬件设计要点 上个月调一块FPGA加速卡插到服务器上怎么都枚举不出来dmesg里连设备ID都看不到。从原理图到PCB再到驱动翻了半天最后发现是参考时钟走线被分割了个坑——但真正把这套东西吃透还是得从PCIe基础知识说起。PCIe是现在几乎所有处理器系统里最核心的高速互连总线独显、NVMe固态、万兆网卡、FPGA加速卡通通挂在它上面。这篇是系列的第一篇面向硬件工程师、驱动开发者和刚入门的学生我不打算把协议栈一页页念给你听而是从“系统怎么把设备认出来、数据怎么在链路上跑起来、板级设计为什么有这些规矩”几个角度把PCIe的整体框架讲清楚。看完之后再去看PCI Express Base Specification或Xilinx/Intel的手册你会发现很多概念已经串成了一条线。1. 从系统结构说起PCIe到底在电脑里扮演什么角色1.1 根复合体、交换机、端点设备一张图看懂总线拓扑PCIe取代了老掉牙的并行PCI总线核心变化就是“点对点串行”。老PCI是共享总线所有设备挂在一组并行线上同一时刻只有一对设备能通信PCIe则是每个设备独占一条串行链路想聊就能聊不用等别人说完。系统里最顶层的设备叫根复合体Root ComplexRC一般集成在CPU里或者由CPU和芯片组共同实现。RC下面可以挂两类东西一类是真正的功能设备也就是端点设备Endpoint比如显卡、NVMe硬盘、网卡另一类是交换机Switch我习惯叫它PCIe交换机或PCIe Switch作用是把一个上游端口扩展出多个下游端口让更多的设备能挂到总线上。交换机内部其实是由若干“PCIe桥”virtual PCI-PCI bridge组成的每个下游端口都是一个独立桥这样操作系统枚举的时候逻辑上就像看到了一组PCI桥串联在一起。很多人调试时看到lspci输出里有PCI bridge设备不明白是啥其实多半就是CPU内部的root port或者Switch的下游端口。每个设备都有唯一的地址格式是“总线号:设备号:功能号”简写BDF。比如01:00.0意思是总线1、设备0、功能0。这个地址就是后面枚举和驱动绑定的基础。整棵拓扑树最多能容纳256条总线、每条总线32个设备、每个设备8个功能理论上的上限就是65536个可寻址功能——当然实际系统受物理插槽和资源限制不可能挂这么多但地址空间设计得非常大足够用了。1.2 接口形态的演变从mini PCIe到M.2为什么会有这么多“长相”PCIe电接口的物理形态非常多样最直观的是主板上那些长长短短的插槽x1、x4、x8、x16数字代表链路宽度也就是有多少对收发差分线。x16插槽通常用于显卡x1/x4用于网卡、采集卡、转接卡。移动设备和紧凑型设备上mini PCIe曾经很流行就是笔记本里那种52pin的小金手指接口走PCIe x1、USB 2.0和SMBus。这几年它基本被M.2取代了。M.2的接口不叫金手指是一排卡扣式的金触点根据Key防呆口位置不同可以走PCIe x2/x4、SATA或USB。举个例子很多WiFi网卡是M.2 Key E接口走PCIe x1加USBNVMe固态是M.2 Key M走PCIe x4。热词里问“网卡mini PCIe接口和M2接口有什么区别”简单说就是mini PCIe物理上固定支持PCIe x1而M.2更像一个“通用插座”能承载PCIe、SATA、USB多种协议尺寸也更灵活有2230、2242、2280等规格数字就是宽度和长度mm。半高挡板也是个容易踩坑的点。标准全高挡板大约120mm半高挡板大约80mm具体各厂商会有细微差异。做项目或买转接卡时一定要对着数据手册量孔位别拍脑袋买否则装机时挡板高出机箱一截机箱盖都合不上。形态是易变的但底层协议是统一的所以接下来要进入正题PCIe是怎么设计成“能干活”的。2. 分层协议栈事务层、数据链路层、物理层各自管什么2.1 发一个读请求要经过几道关卡PCIe协议分层跟网络协议栈有点像从上到下分事务层Transaction Layer、数据链路层Data Link Layer、物理层Physical Layer。可以打这么个比方事务层是老板负责决定“要发什么货”数据链路层是仓库负责打包、贴单子、保证不丢件物理层是卡车司机负责把包裹从A地运到B地。当CPU要读某个设备的寄存器或内存软件只需要发起一次地址访问剩下的交给根复合体。RC的事务层把这个请求封装成一个TLPTransaction Layer Packet事务层包里面写清楚是读还是写、目标地址、请求者是谁、多长数据。TLP往下传给数据链路层链路层给它加上序列号和LCRC校验码然后交给物理层变成差分信号发出去。对端设备收到后物理层先把比特流还原成字节链路层校验CRC没问题就丢给事务层解析。如果是一个读请求接收方随后要构造一个Completion完成报文回到请求者那里把读到的数据带回去。整个过程中软件并不关心数据走的是哪条通道、经过几个Switch这全靠协议分层把它隐藏掉了。2.2 TLP长什么样从Header到LCRCTLP是PCIe通信中最核心的“包裹”。一个TLP的头部通常是12字节3DWDW双字4字节有些需要64位地址或复杂扩展的会到16字节4DW甚至更长。头部后面跟着可选的载荷Payload最大可以到4096字节最后还有可选的ECRC尾巴。链路层还会在TLP外面再套一层——加2字节序列号和4字节LCRC这才是真正在物理链路上跑的格式。头部里最重要的几个字段Fmt/Type告诉接收端这是Memory读写、IO读写、配置读写、Completion还是消息报文并且区分带不带数据。Length表示数据长度单位是DW最小1个DW最大1024个DW。Requester ID发起者的BDF相当于“谁寄的”。Tag标签号用来匹配请求和对应的Completion。Last/First DW Byte Enable标记数据中哪些字节有效。Address或BAR信息读写的目标地址。配置读写比较特殊它使用Requester ID、Destination ID和寄存器偏移地址去访问设备的配置空间。配置空间是每个PCIe设备必有的“身份证和户口本”下面讲到枚举时还要展开。2.3 链路层和物理层的“隐形工作”数据链路层主要管两件事可靠传输和流量控制。TLP被加上序列号发出去后接收端每收到一个合法的TLP都要回复ACK如果发送端发现NAK了或者超时没收到ACK就会重传。这就是PCIe链路层把“丢包”兜住的机制。此外链路层还维护基于信用的流控credit-based flow control防止发送端一股脑把数据灌过来把接收端缓冲塞爆。物理层又分逻辑子层和电气子层。逻辑子层负责字节拆分、扰码、编码电气子层负责差分信号的发送和接收。物理层里还有个重要状态机叫LTSSM下一段我会展开。编码这件事值得先说清楚PCIe Gen1和Gen2用8b/10b编码每8位有效数据占10个bit有20%的开销到了Gen3改用128b/130b编码每128位有效数据只占130个bit开销只有约1.5%。这就是为什么Gen3起速率跳了一大截编码效率提升是重要原因之一。信号完整性上Gen1/Gen2直接传输NRZ信号Gen3以后为了在更高速率下保持信号质量引入了发送端去加重和接收端均衡。很多从串行总线转过来的工程师只看速率不看编码很容易把带宽算错后面我会专门给一张带宽速查表。3. 链路训练与枚举设备是怎么被系统发现的3.1 LTSSM状态机从复位到L0的握手过程PCIe设备上电后物理链路并不会立刻就能传数据必须先经过一个“握手协商”过程这就是LTSSM状态机。名词很唬人实际可以理解成两个设备见面对暗号先确认对方存在再说清楚自己能支持多快、可以用几对线最后对齐成功进入正常工作状态L0。流程大致是设备从复位出来进入Detect状态发送端在链路上发出信号看看对面有没有设备响应确认有设备后进入Polling状态双方互发TS1和TS2训练序列协商链路速度和宽度。默认先跑Gen1的2.5GT/s如果双方都支持更高的速率再通过训练序列里的数据速率标识升级到Gen2、Gen3或更高。链路宽度也是这时候定下来的——比如一个x16插槽插了x8带宽的设备训练后最终会停在x8。协商完成后进入Configuration状态把每条lane的编号对齐、处理极性翻转随后进入L0正常通信。调试时如果设备跑在x1而不是x16或者一直都只有Gen1大概率是训练协商出了问题比如有一对差分线没连好、时钟有问题或者插槽和卡的金手指接触不良。LTSSM是一个大话题后面排障章节我会再给一些具体建议。3.2 配置空间和枚举过程BDF地址怎么分配每个PCIe设备都有一块配置空间前256字节是完全兼容PCI时代的从0x100开始还有PCIe扩展配置空间。这块空间就是软件的“户口本”里面写着你是谁Vendor ID、Device ID、你能干什么Class Code、你需要多大空间BAR寄存器、你支持哪些高级功能Capability链表等等。枚举是软件层面的活由BIOS/UEFI或操作系统内核在启动阶段完成。大致过程如下从总线0开始逐个扫描设备。对每个设备读取Vendor ID和Device ID。如果读回来全是0xFF说明这个设备号上没东西如果读回来全0说明设备存在但还没完成初始化。接着看Header Type。如果最高位是0说明是一个单功能设备如果是1说明是多功能设备需要扫描接下来的功能号。如果这是一个PCIe桥Header Type是0x01软件就会配置它的Primary/Secondary/Subordinate bus number给它分配一个新的总线号然后递归去扫描下一层总线上的设备。对于每个端点设备软件会读取它的BAR基地址寄存器搞清楚它要多大内存/IO空间然后把这些地址空间映射到系统物理地址空间里。最后遍历Capability链表识别设备支持的各种特性比如MSI中断、AER高级错误报告、SR-IOV然后加载对应的驱动程序。BAR的探测方法很经典软件往BAR寄存器写全1再读回来从读到的最低有效位位置就能推算出设备需要多大地址空间。比如写0xFFFFFFFF读回0xFFFF0000说明地址必须是64KB对齐需要64KB空间。这个方法面试爱考实际调驱动时也会用到。3.3 带宽协商Gen1到Gen6链路速率和宽度PCIe每一代速率都有严格定义目前主流是Gen3和Gen4Gen5正在服务器和高端PC铺开Gen6已经在路上。下表把各代的单链路速率和编码方式整理出来注意GT/s是“每秒十亿次传输”PCle是双单工同一时刻收发各走各的差分对。代际单lane速率编码方式单lane带宽(约)x16总带宽(约)Gen12.5 GT/s8b/10b250 MB/s4 GB/sGen25 GT/s8b/10b500 MB/s8 GB/sGen38 GT/s128b/130b985 MB/s15.75 GB/sGen416 GT/s128b/130b1969 MB/s31.5 GB/sGen532 GT/s128b/130b3938 MB/s63 GB/sGen664 GT/sPAM4编码7877 MB/s126 GB/s注意表里带宽是“约”实际和厂商宣传值会有出入原因下面会讲。另外还有一个容易踩的概念x16指的是链路宽度也就是16对差分收发线而插槽机械尺寸上x16往往兼容x8和x4的设备最终速度由训练协商结果决定。4. 带宽计算标称速率和实际吞吐量之间的那道坎4.1 编码开销是怎么吃掉带宽的每次看到厂商标称“PCIe 4.0 x16带宽32GB/s”我都在心里默默打个折。因为标称值用的是原始比特率16GT/s乘16条lane再除以8换算成字节得到32GB/s。但这没有算编码开销也没有算协议本身的各种报文开销。编码开销最直观Gen1/Gen2用8b/10b编码意味着每传输8位有效数据实际上要发10个bit有效效率只有80%。Gen3/Gen4用128b/130b效率约98.46%。所以PCIe 4.0 x16的真实原始有效数据率大概是16GT/s × 16 × (128/130) ÷ 8 ≈ 31.5GB/s比32GB/s少了一点。还没完。链路正常工作时还要周期性发送SKP有序集用于时钟补偿TLP之间也有帧标记和空闲时间ACK/NAK、流控更新等DLLP都要占带宽小包传输时TLP头部和CRC占的比例更大。所以实际能跑到的吞吐量往往只有理论有效带宽的80%到90%左右。做带宽评估时别按宣传值要按这个折扣去算否则前期规划就给后面埋坑。4.2 实际可用带宽如何估算给出一个粗略的估算公式单lane有效带宽(MB/s) 速率(GT/s) × 编码效率 × (1000/8)这里的1000/8是为了把GT/s换算成每秒兆字节数按十进制。如果做嵌入式开发习惯用MiB/s按1024要把1000换成1024数值会更小一点。以Gen3 x4为例8GT/s × (128/130) × 1000/8 ≈ 984.6MB/s再打个九折实际吞吐大概在850到900MB/s之间。我调NVMe固态时Gen3 x4的盘读Sequential大约能跑到3.4GB/s左右比理论的3.94GB/s低不少但比单纯拿4GB/s宣传值去要求驱动要现实得多。4.3 用lspci查看当前链路状态Linux下最常用的查看链路状态工具是lspci。要看到详细链路协商结果用-vvv模式输出里会有类似这样的内容LnkCap: Port #1, Speed 32GT/s, Width x16, ASPM not supported LnkSta: Speed 16GT/s, Width x16LnkCap是设备能力上限LnkSta是当前实际协商结果。如果一块支持Gen4 x16的显卡插在主板上显示LnkSta只有Speed 8GT/s、Width x4那链路训练就出了问题需要查插槽、转接线或者BIOS设置。setpci可以更直接地读写配置空间比如用setpci -s 01:00.0 CAP_EXP10.w看链路状态字段适合脚本自动化检查但平时查问题用lspci -vvv就够了。5. 硬件设计中的几个“送命题”阻抗、耦合电容、等长5.1 85Ω差分阻抗为什么PCIe不按100Ω来传统的USB、以太网、HDMI很多都按90Ω或100Ω差分阻抗来设计但PCIe标准要求的是85Ω差分阻抗。这个值不是拍脑袋定的PCIe信号幅度低、速率高85Ω可以在功耗、信号幅度和走线损耗之间取一个平衡。PCB上做PCIe走线时叠层工程师一般都会按85Ω去控制线宽线距差分对周围要有完整的地平面。实际项目中常见问题是板厂把PCIe和别的接口当成同一种差分线来加工结果阻抗测出来是100Ω左右差值大了会导致反射眼图变差轻则链路降速重则直接训练失败。所以投板前一定要在PCB叠层文件里明确标注“PCIe差分阻抗85Ω”并要求板厂用TDR抽测。没有网络分析仪或TDR时至少要让板厂提供阻抗报告不要省这道工序。5.2 AC耦合电容摆在哪一侧容值选多大PCIe的电气规范要求差分链路必须串接AC耦合电容作用很简单隔直。发送端和接收端的直流工作电平可能不一样直接耦合会把两边的直流偏置搅在一起而电容能让交流信号通过把直流分量挡住。电容一般放在发送端一侧也就是常说的靠近driver端具体原因是为了让收端看到更干净的信号源。容值常见100nF也有220nF规范上有最小值要求通常不要低于75nF否则对低频分量衰减太大会让眼图劣化。贴装时要用小封装0402或0201并且要特别注意焊盘的寄生电容和阻抗不连续。我之前见过有设计把耦合电容放在走线末端、离连接器很近结果因为stub和焊盘引发反射Gen3死活训不上改成靠近主控端后问题就消失了。5.3 对内等长和对外等长别把概念混在一起热词里有一句“pcie的发送差分对间需不需要等长”每次看到都想认真答一下这里其实分两个层级的问题。第一层是“对内等长”也就是一对收发差分线里面的P和N两条线要尽量等长。因为差分信号是靠两条线上的电压差来判别的P和N长度差太大会让共模转差模造成信号质量下降。PCIe对内的skew要求非常严常见设计规则是控制在5到10mil以内速率越高越严。第二层是“lane间等长”也就是不同lane之间要不要等长。这个就宽松多了因为PCIe协议在接收端有专门的deskew机制链路训练和正常传输时会做lane-to-lane对齐补偿不同lane的传输延迟差。也就是说即使x4链路的四根lane之间长度差了个几百mil甚至上千mil一般也不会直接导致功能失败只会在延迟和信号裕量上付出一点代价。但我不建议把宽松当借口板级设计时依然会尽量让lane之间保持大致等长毕竟谁也不想把自己的产品压着规范下限跑。还有一个小知识点PCIe允许lane极性反转也允许lane在物理上顺序打乱接收端在Configuration状态会自动纠正。这是布线的一大福音尤其是金手指连接器处扇出困难时可以灵活地交换P/N或lane顺序不必为了对齐而绕得死去活来。6. 时钟与弹性缓冲频率偏差是怎么被吸收的6.1 100MHz Refclk 和SRIS/SRNSPCIe系统通常提供一个100MHz的参考时钟Refclk所有收发器都拿它作为频率基准。经典架构叫Common Clock共用时钟就是一颗晶振或时钟发生器把100MHz同时送给多个设备大家用同一个基准频率一致性好。后来为了布线灵活出现了Independent Refclk架构各设备可以自带时钟协议要求两边的频率误差在±300ppm以内。再后来又有了SRISSeparate Reference Clock with Independent Spread Spectrum既允许独立时钟还允许两边分别做展频。参考时钟在硬件设计里的地位很高它直接决定了链路能否稳定运行。很多板卡调试失败都是Refclk没起振、电平不对、或者走线太长导致信号衰减。Refclk也是差分走线通常也要串AC耦合电容而且摆位要靠近接收端干扰源要避开。6.2 Elastic Buffer 和 SKP 有序集为什么需要弹性缓冲因为即使共用一颗100MHz晶振不同芯片的PLL恢复出来的数据时钟也不可能完全一致更别说独立时钟或SRIS了。发送端按自己的时钟发数据接收端按自己的或恢复的时钟采样两边的频率差会导致数据流里的比特数相对接收端逐渐变多或变少如果不处理缓冲区迟早会溢出或读空。解决办法就是弹性缓冲Elastic Buffer。接收端有个FIFO数据以恢复时钟写入以本地读时钟读出。当两边频率有微小偏差时FIFO的水位会慢慢漂移。PCIe协议规定发送端要周期性地插入SKP有序集SKP Ordered Set这是一小段由特定字符组成的填充数据。接收端识别到SKP OS后根据FIFO水位决定是删除一个SKP、多发一个SKP、还是原样保留从而把水位拉回安全区间。这个过程对上层完全透明数据不会被误删因为SKP本身不携带有效信息。做FPGA PCIe调试时如果ELB或SKP处理逻辑写得不严谨最常见的表现就是链路不断进入Recovery或者偶发CRC错误。我去年调一块用独立时钟的FPGA板卡系统运行几分钟就报AER错误把时钟改成展频模式后再调SKP处理问题才消失。所以遇到“随机性地隔一阵子报错”的疑难杂症优先怀疑时钟架构和弹性缓冲的配置而不是死磕数据通路上肉眼可见的焊点。7. 常见问题速查与排障思路7.1 枚举不到设备系统完全找不到设备排查顺序基本是供电、时钟、复位、链路信号。先量设备端有没有正常供电再看100MHz参考时钟有没有波形、频率对不对接着检查复位信号有没有被拉低或反复复位最后上示波器测PCIe差分信号是否正常。如果都没有问题那就看软件层面——BIOS里PCIe port有没有被禁用、枚举时有没有被卡住可以在内核启动参数里加pcirealloc或者pcinomsi再做一次。很多新手容易忽略接触问题。转接卡、插槽氧化、金手指脏了都会导致Detect阶段失败看起来就像没插卡一样。实测下来把卡拔下来用酒精擦一遍金手指往往比翻半天配置有效得多。7.2 链路跑在x1而不是x16物理上插了x16的卡系统里却显示x1这种问题常见于转接卡和服务器背板。原因可能是设备本身只有x1能力也可能是训练协商时只有一对lane能通过检测。排查时先看lspci -vvv的LnkCap和LnkSta确认设备能力到底是多少若设备的LnkCap本身就是x16、但LnkSta停在x1大概率是某条lane的差分对不通或者连接器孔位氧化。遇到这种情况可以用示波器量每对lane的接收端有没有信号找到断的那一对。另一种可能是BIOS里给某个插槽强制设了x1或Gen1有些服务器为了兼容性会默认这样做改回Auto或者设置更大的宽度就能解决。7.3 驱动加载异常与ASPM的坑设备能枚举到但驱动加载失败或者吞吐忽高忽低先去看dmesg里有没有AER打头的错误。很多时候是ASPMActive State Power Management在捣鬼系统为了省电把链路切到低功耗状态结果切来切去把传输切出了错误。临时排查可以直接用内核参数pcie_aspmoff如果问题消失就可以定位到ASPM或上游策略的问题。还有一类热词里的问题Realtek PCIe网卡驱动说“不支持Vista”。这种多半是系统版本或服务包太旧或者下载错了驱动分支。硬件本身是PCIe接口的驱动包和系统组件不匹配而已换对应版本或者升级系统补丁就好。举这个例子的意思是PCIe设备在系统里的行为不光是总线层的责任电源管理、中断、驱动栈都会干扰表现排查时要放开视野。现象优先排查项常用命令/手段设备完全枚举不到供电、Refclk、复位、接触示波器测量lspci确认链路降速或窄宽差分线、连接器、BIOS设置lspci -vvv看LnkCap/LnkSta随机CRC/AER错误时钟架构、ELB/SKP、SI问题dmesg协议分析仪抓包驱动加载失败版本、ASPM、MSI设置pcie_aspmoff更换驱动BAR资源不足BIOS资源分配内核参数pcireallocPCIe这块知识体系很大但基础框架并没有那么玄乎只要把系统拓扑、分层协议、链路训练、配置空间和板级设计这几个环扣住后面的高级话题——SR-IOV、ATS、DMA、原子操作、错误恢复——都能挂在这棵树上慢慢长出来。我个人建议不要一上来就啃规范的几百页细节先照着文档或开发板完整跑一遍枚举再拿协议分析仪抓一次TLP很多抽象概念立刻就具体了。这套系列我打算按话题往后写下一篇会专门拆事务层TLP的各种类型和报文格式顺带讲Completion的匹配规则那部分对写驱动和FPGA逻辑的人帮助最大。你手头如果有PCIe调不通的具体场景欢迎留言我可以挑典型的故障案例写进后面的文章里。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进