上个月调一块FPGA加速卡,插到服务器上怎么都枚举不出来,dmesg里连设备ID都看不到。从原理图到PCB再到驱动翻了半天,最后发现是参考时钟走线被分割了个坑——但真正把这套东西吃透,还是得从PCIe基础知识说起。
PCIe是现在几乎所有处理器系统里最核心的高速互连总线,独显、NVMe固态、万兆网卡、FPGA加速卡,通通挂在它上面。这篇是系列的第一篇,面向硬件工程师、驱动开发者和刚入门的学生:我不打算把协议栈一页页念给你听,而是从“系统怎么把设备认出来、数据怎么在链路上跑起来、板级设计为什么有这些规矩”几个角度,把PCIe的整体框架讲清楚。看完之后,再去看PCI Express Base Specification或Xilinx/Intel的手册,你会发现很多概念已经串成了一条线。
1. 从系统结构说起:PCIe到底在电脑里扮演什么角色
1.1 根复合体、交换机、端点设备:一张图看懂总线拓扑
PCIe取代了老掉牙的并行PCI总线,核心变化就是“点对点串行”。老PCI是共享总线,所有设备挂在一组并行线上,同一时刻只有一对设备能通信;PCIe则是每个设备独占一条串行链路,想聊就能聊,不用等别人说完。
系统里最顶层的设备叫根复合体(Root Complex,RC),一般集成在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加USB;NVMe固态是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的事务层把这个请求封装成一个TLP(Transaction Layer Packet,事务层包),里面写清楚是读还是写、目标地址、请求者是谁、多长数据。TLP往下传给数据链路层,链路层给它加上序列号和LCRC校验码,然后交给物理层变成差分信号发出去。
对端设备收到后,物理层先把比特流还原成字节,链路层校验CRC,没问题就丢给事务层解析。如果是一个读请求,接收方随后要构造一个Completion(完成报文)回到请求者那里,把读到的数据带回去。整个过程中,软件并不关心数据走的是哪条通道、经过几个Switch,这全靠协议分层把它隐藏掉了。
2.2 TLP长什么样:从Header到LCRC
TLP是PCIe通信中最核心的“包裹”。一个TLP的头部通常是12字节(3DW,DW=双字=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和Gen4,Gen5正在服务器和高端PC铺开,Gen6已经在路上。下表把各代的单链路速率和编码方式整理出来,注意GT/s是“每秒十亿次传输”,PCle是双单工,同一时刻收发各走各的差分对。
| 代际 | 单lane速率 | 编码方式 | 单lane带宽(约) | x16总带宽(约) |
|---|---|---|---|---|
| Gen1 | 2.5 GT/s | 8b/10b | 250 MB/s | 4 GB/s |
| Gen2 | 5 GT/s | 8b/10b | 500 MB/s | 8 GB/s |
| Gen3 | 8 GT/s | 128b/130b | 985 MB/s | 15.75 GB/s |
| Gen4 | 16 GT/s | 128b/130b | 1969 MB/s | 31.5 GB/s |
| Gen5 | 32 GT/s | 128b/130b | 3938 MB/s | 63 GB/s |
| Gen6 | 64 GT/s | PAM4编码 | 7877 MB/s | 126 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_EXP+10.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/SRNS
PCIe系统通常提供一个100MHz的参考时钟(Refclk),所有收发器都拿它作为频率基准。经典架构叫Common Clock(共用时钟),就是一颗晶振或时钟发生器把100MHz同时送给多个设备,大家用同一个基准,频率一致性好。后来为了布线灵活,出现了Independent Refclk架构,各设备可以自带时钟,协议要求两边的频率误差在±300ppm以内。再后来又有了SRIS(Separate 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有没有被禁用、枚举时有没有被卡住,可以在内核启动参数里加pci=realloc或者pci=nomsi再做一次。
很多新手容易忽略接触问题。转接卡、插槽氧化、金手指脏了都会导致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打头的错误。很多时候是ASPM(Active State Power Management)在捣鬼,系统为了省电把链路切到低功耗状态,结果切来切去把传输切出了错误。临时排查可以直接用内核参数pcie_aspm=off,如果问题消失,就可以定位到ASPM或上游策略的问题。
还有一类热词里的问题:Realtek PCIe网卡驱动说“不支持Vista”。这种多半是系统版本或服务包太旧,或者下载错了驱动分支。硬件本身是PCIe接口的,驱动包和系统组件不匹配而已,换对应版本或者升级系统补丁就好。举这个例子的意思是:PCIe设备在系统里的行为,不光是总线层的责任,电源管理、中断、驱动栈都会干扰表现,排查时要放开视野。
| 现象 | 优先排查项 | 常用命令/手段 |
|---|---|---|
| 设备完全枚举不到 | 供电、Refclk、复位、接触 | 示波器测量,lspci确认 |
| 链路降速或窄宽 | 差分线、连接器、BIOS设置 | lspci -vvv看LnkCap/LnkSta |
| 随机CRC/AER错误 | 时钟架构、ELB/SKP、SI问题 | dmesg,协议分析仪抓包 |
| 驱动加载失败 | 版本、ASPM、MSI设置 | pcie_aspm=off,更换驱动 |
| BAR资源不足 | BIOS资源分配 | 内核参数pci=realloc |
PCIe这块知识体系很大,但基础框架并没有那么玄乎:只要把系统拓扑、分层协议、链路训练、配置空间和板级设计这几个环扣住,后面的高级话题——SR-IOV、ATS、DMA、原子操作、错误恢复——都能挂在这棵树上慢慢长出来。我个人建议不要一上来就啃规范的几百页细节,先照着文档或开发板完整跑一遍枚举,再拿协议分析仪抓一次TLP,很多抽象概念立刻就具体了。
这套系列我打算按话题往后写:下一篇会专门拆事务层TLP的各种类型和报文格式,顺带讲Completion的匹配规则,那部分对写驱动和FPGA逻辑的人帮助最大。你手头如果有PCIe调不通的具体场景,欢迎留言,我可以挑典型的故障案例写进后面的文章里。