ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PCIe Crosslink详解:从NTB到FPGA实现的高速互连方案

PCIe Crosslink详解:从NTB到FPGA实现的高速互连方案 1. 两个根复合体之间怎么通信PCIe Crosslink的本质与适用场景1.1 从“CPU到外设”到“端点互连”的视角转换我做PCIe相关开发这些年被问得最多的一个问题是“两个CPU或者一个CPU一个FPGA之间要高速互联除了以太网还有没有别的路子”答案是有的就是PCIe Crosslink。大多数人对PCIe的印象还停留在“主板上插显卡、插NVMe盘”的层面总觉得它是CPU用来挂外设的。但PCIe本身就是一种点对点的串行链路协议理论上它不需要一个“中心”来仲裁两端都可以是根复合体Root ComplexRC也可以是端点Endpoint。Crosslink做的就是这件事把两个具备RC能力的设备用PCIe链路直接对接起来让两侧的地址空间互相可见数据不需要经过以太网或者共享内存中转。这里先明确一下概念。PCIe Crosslink字面意思是“交叉链接”在实际工程里通常指两条独立PCIe层次域之间的互联。最常见的形式是CPU A和CPU B各带一个RC通过PCIe链路把两个RC连起来另一种更常见的形态是一个带PCIe硬核的FPGA与CPU通过NTBNon-Transparent Bridge非透明桥对接两端各自看到一块“伪设备”通过这块伪设备上的窗口来交换数据。它和普通PCIe链路的本质区别在于普通链路是RC向下枚举Endpoint链路两端是不对等的Crosslink则要求链路两端都有“主”的能力或者至少一端通过非透明桥模拟出这种能力。什么场景下会用到Crosslink我在实际项目里遇到过这几类双路服务器主板上的CPU间高速互联虽然大多走UPI/QPI但有些异构平台会用PCIe做备份通道。多主机系统中一台x86主机和一台FPGA加速卡之间需要无锁的共享内存式通信不希望每个包都过网卡、过协议栈。带PCIe交换机的存储阵列两个控制器每个控制器是一个RC之间需要镜像和锁同步用Crosslink方式省掉专用同步线缆。虚拟化环境中两个物理主机通过Crosslink直连可以做成低延迟的VM间通信通道。如果只是偶尔传几十MB数据网卡完全够用但你一旦需要微秒级延迟、每通道几十GBps的带宽、以及像访问本地内存一样访问远端内存Crosslink就是绕不开的方案。1.2 非透明桥NTB为什么是Crosslink的关键要把两个RC连起来第一个想到的问题就是“两个CPU都认为自己是老大谁来枚举谁”普通PCIe的枚举过程是RC从总线0开始往下扫描设备、分配BDF、配置BAR。如果两个RC直接硬连两边都会把自己当成根扫描不到对面时就会报错而且即使扫描到了也会因为地址空间冲突而乱套。所以工程上不会让两个RC“裸连”而是在中间加一层非透明桥这就是NTB。NTB不是让两个RC真正合并成一个拓扑而是在硬件层面把两端隔离成两个独立的PCIe域然后通过一组“窗口”把一侧的地址区域映射到另一侧的地址区域。每一侧RC看到的都是自己域中的一个普通EP设备这个设备就是NTB逻辑但它实际上不是真正的端点而是一扇“门”——你往这扇门里写数据硬件会把数据搬运到另一侧的地址窗口。所以Crosslink的核心就是NTB。你需要把他配置成左侧CPU往BAR窗口写0x1000地址右侧CPU读自己的BAR窗口地址也能拿到同样的数据。这种窗口通常叫Memory Window配合Doorbell门铃寄存器用于中断通知配合Scratchpad暂存寄存器用于小数据交换和握手。理解了这几个概念Crosslink就算入门了。2. 透明桥与NTB的区别决定Crosslink的枚举与地址映射2.1 透明桥的局限性为什么两个RC不能直接搭在一起我们先看看PCIe桥的两种类型。透明桥Transparent Bridge是PCIe体系里最常见的桥它的工作是“透明地”把上游总线的枚举请求继续向下游转发不修改地址。CPU配置的每一个ECAMEnhanced Configuration Access Mechanism读请求发到透明桥透明桥就根据内存/IO基地址寄存器判断要不要继续往下发。这样一来系统里所有设备和内存都在同一个地址域里CPU可以枚举出整棵设备树。但这种透明性有个致命前提整个拓扑必须是一棵以RC为根的树不能有环路也不能有两个根。如果两个RC通过透明桥互连就出现了“两个根争抢总线号”的局面。即使你强制规定一侧是上游另一侧作为下游设备处理那另一侧RC上的所有本地内存和MMIO资源都暴露在一个完全独立的地址空间里但PCIe枚举机制无法处理“一个BAR背后来着另一个CPU的整个4GB内存空间”这种映射——因为BAR本身就最多几十KB到几百MB而且当另一端CPU的内存物理位置不确定时地址转换就是一笔糊涂账。更麻烦的是两个CPU的DMA请求可能互相访问对方的PCIe配置空间造成不可预期的系统错误。所以结论很明确在两个RC之间透明桥方案是走不通的必须在硬件层把两边“隔离”再开一扇可控的窗户。这扇窗户就是NTB。2.2 NTB的地址窗口Memory Window与BDF分配以Intel的NTB驱动和Xilinx的NTB IP为例NTB的寄存器模型通常包含BAR空间、地址窗口ATWAddress Translation Window和Doorbell。BAR空间是每一侧CPU看到的“设备”的基地址地址窗口决定你写的这个偏移量会被硬件重映射到对端物理地址的哪个位置Doorbell则是往对方门铃寄存器写一次就能触发对方的中断。实际配置过程中你需要为两侧窗口分配不同的目标地址。举个例子如果左侧CPU希望访问右侧CPU的一段内存区域比如3GB物理地址长1GB你就需要在左侧设备的某个Address Translation Window中设置一个本地偏移硬件会把落在该偏移范围内的TLP请求翻译成右侧物理地址空间的访存事务。这里的关键参数有四个本地BAR基址左侧CPU设备树里看到的MMIO地址。窗口起始偏移在BAR基础上加多大的偏移进入该窗口。窗口大小建议按照实际的DMA缓冲池大小来配不是越大越好。对端物理地址窗口数据最终要映射到右侧CPU的哪个物理地址。这个映射关系有点类似于操作系统的页表只不过它由硬件直接完成不经过CPU的TLB。配置好了之后左侧CPU写它的BAR窗口本质上等同于向右侧CPU物理地址发起写请求。2.3 枚举顺序与BIOS/ECAM的处理接下来是枚举。在Crosslink拓扑中每个RC仍然独立枚举自己的域。左侧CPU枚举到“NTB设备”看到的是带有特定Vendor ID和Device ID的Endpoint右侧CPU也枚举到同样一个“NTB设备”但它们的BDF互不相干。比如左侧给NTB分配的BDF是00:1f.0右侧可能是80:00.0这都没关系设计上层软件时要用驱动来跨越这种差异。但有一个细节非常容易踩坑当BIOS做内存初始化时它会扫描PCIe总线并给各个设备的BAR分配物理地址。如果Crosslink链路在BIOS阶段还没完成链路Training比如参考时钟慢了或者对端没准备好BIOS就看不到这个设备自然也就不会预留足够的MMIO空间。结果是驱动后来加载时再去配置BAR你会拿到“resource busy”或无法分配地址的错误。所以Crosslink系统的BIOS配置里通常需要强制分配足够大的MMIO空间比如4G以上甚至要把BAR窗口预留地址固定下来而不是交给BIOS自动分配。而且由于两个RC都要访问同一段“物理上属于谁”的地址你的系统还必须处理内存地址别名的问题。最简单的方式是把Crosslink窗口映射到一段固定的MMIO地址区这段地址对两个CPU而言都只是一段“设备IO空间”不映射到内存这样就不存在缓存一致性问题代价是访问速度会略慢一些。如果要做高性能传输就必须在驱动里做Cache Coherent缓存一致性处理比如x86上使用Uncached或Write-Combining映射或者干脆用DMA引擎来完成搬运避免CPU直接去读远端内存。3. 实现Crosslink的三种主流路径FPGA、Switch、CPU原生接口3.1 FPGA方案用Xilinx/Intel的NTB IP以及自研逻辑的边界做FPGA加速卡的朋友应该都熟悉Xilinx现在是AMD的XDMA IP、Intel的VTP/NTB IP。用FPGA实现Crosslink最大的优势是灵活你可以自己定义NTB设备里除了标准寄存器之外的业务逻辑比如在窗口基础上叠加数据格式转换、带外控制通道、甚至直接做一个硬件消息队列。如果你用的是Xilinx UltraScale/Versal系列的PCIe硬核它的NTB IP支持Endpoint和Root Port配置。但请注意硬核NTB IP的License和配置流程都比较复杂而且针对特定芯片。它的寄存器组里有完整的NTB配置空间、错误处理、Doorbell和Scratchpad。使用这套IP时你需要重点关心两个点链路宽度协商FPGA的PCIe hard block通常支持x8/x16但Crosslink对端的CPU可能只支持x16。要确保两端都能以x16协商否则只能降速到x8性能少一半。地址窗口数量的上限不同IP版本支持的地址窗口个数不同常见4~8个。逐个窗口配满看起来很爽但窗口越多地址转换逻辑越复杂时延也越高。性能和灵活性是要权衡的。如果你不想用官方IP想自己在FPGA里用GT Transceiver裸写PCIe物理层再实现Transaction Layer和NTB逻辑那我劝你慎重。PCIe物理层对齐、加扰、弹性缓存这些事已经够烦了NTB还需要维护完全独立的两套配置请求者ID和标签处理逻辑更容易出错。除非你有专门的PCIe IP核设计团队否则不要从零开始。3.2 PCIe Switch的NTB mode适合多host互连PCIe Switch比如Microchip的PM8532B、Broadcom的PEX系列、这几家都有支持NTB的型号通常工作在多端口透明交换模式。但有些Switch芯片支持NTB mode可以把某个上游口配置成“非透明”让一个系统同时支持两到四个RC主机互连。这种方案的好处是它把Crosslink连接从“点到点”扩展成“多对多”非常适合存储控制器、多host服务器等应用。具体来说Switch的一个端口作为透明上游连接主机A另一个端口作为透明上游连接主机B两者都枚举到同一台Switch中的不同逻辑设备其中一个设备工作为NTB。主机A向NTB设备的窗口写数据Switch内部把数据转发到主机B的地址窗口。由于Switch内部的crossbar转发延迟很低这种跨主机访问的延迟能做到亚微秒级别。用Switch做Crosslink的坑主要在配置阶段。你必须在BIOS或EEPROM里面设置Switch的端口角色、NTB使能位、窗口映射表。很多工程师把它当普通Switch用结果两个主机各自枚举到设备却不互通就是因为没开NTB mode没配窗口映射。这里我建议在调试时先读一下Switch的 datasheet里的NTB寄存器初始化示例严格按它的步骤走不要自己凭感觉写。3.3 CPU原生Crosslink某些平台可以但代价是隐藏的有些CPU平台本身支持把两个CPU的PCIe端口直接连接起来比如某些服务器芯片组提供“PCIe Peer-to-Peer”或者“Crosslink”模式。这种模式在BIOS里开启后两个CPU的PCIe域可以互相访问而且不需要外部NTB芯片。它看起来最完美但实际使用中限制很多。首先它对CPU型号、步进和BIOS版本有严格限制。不是每颗CPU都支持而且支持它的平台往往要求两端RC的PCIe控制器工作在一个特殊的“非透明模式”下。这个模式会占用两个RC的其中一个控制器导致你少了一个可用的PCIe x16插槽。其次这个模式下的DMA访问路径与本地PCIe路径不是完全等价的它在某些平台上会走一条环形的互连总线比如UPI的一部分带宽和延迟虽然优于千兆网但比不上本地访问。最麻烦的是一旦开启Crosslink模式PCIe热插拔、SR-IOV等高级特性可能会被禁用。所以我的经验是如果产品形态是单板内CPU到FPGA优先选FPGA自带的NTB IP如果产品是一台机器里多主机优先选支持NTB的Switch只有当你正好在同一块主板上做双CPU互连而且平台手册明确支持Crosslink模式才考虑CPU原生方案。至于Intel/AMD桌面平台老实说原生Crosslink支持非常有限别抱太大期望。4. 实际部署中的硬件连接与链路Training问题4.1 拓扑、参考时钟与通道分配Crosslink的PCB设计比普通PCIe设备要严格得多。普通PCIe设备连接的是CPU的Root Port走线长度和拓扑相对固定Crosslink两端都是RC或桥信号质量和时钟抖动对链路的影响会更明显。先说说参考时钟。PCIe允许三种时钟架构Common Clock共用时钟、Separate Reference Clock with SSC独立时钟带扩频、Data Clock数据时钟。Crosslink链路最采用Common Clock方式因为两端都从同一块板子上的单一基准晶振取时钟链路Training时抖动最小。如果两个板卡通过线缆连接那么必须使用专用的PCIe cable和时钟缓冲器确保SSC的调制频率一致。通道分配上Crosslink通常需要对称布局即两侧的收发差分对一一对应。但在FPGA焊接到载板时Pin Assignment可能会打乱通道顺序。PCIe协议中的Lane Reversal机制允许通道顺序翻转比如Tx[0]接到对端Rx[7]但软件通过链路Training时协商出逆转这极大方便了布线。但这不代表你可以在PCB上乱连——通道反转有固定的限制只能整体反转不能中间任意交换。如果引脚分配导致的非对称交叉过于复杂还是老老实实导出Pin Map来人工核对必要时修改逻辑引脚约束。还有一个容易被忽视的点是PCIe半高挡板。如果你做的是标准PCIe插卡形态Crosslink口通过挡板上的连接器引出那一定要参考PCIe Card Electromechanical规范来设计挡板和连接器位置。热词里提到“pcie半高挡板尺寸”这块在机械设计上是不能拍脑袋的挡板上开孔位置稍有偏差插到机箱里就会卡住或者接触不良。我建议直接找立创或者嘉立创的挡板库里面标准半高挡板的尺寸图很齐全省得自己画。4.2 链路训练失败定位LTSSM状态与眼图排查Crosslink调试最大的敌人是链路训练失败。明明两边都是标准PCIe接口为什么连不上最常见的原因是LTSSMLink Training and Status State Machine卡在了某个状态比如Polling或者Configuration。这时候你通过PCIe Analyzer或者逻辑分析仪能看到两侧的Rate Change握手不成功。我曾经的调试经历是这样的某块FPGA板卡与x86主板做Crosslinkx86侧BIOS启动时链路Link Training失败LTSSM一直处于Polling.Active状态。排查发现FPGA侧的100MHz差分参考时钟用了板载晶振但晶振的精度是±100ppm而且没有展频x86主板要求Common Clock架构下参考时钟必须带SSC而且频率容差要在±300ppm内。看起来±100ppm是在允许范围里的但问题是FPGA侧的PLL无法锁定因为x86侧的Root Port在Training期间会发送多个不同频率的SRIS序列FPGA如果没有开启SRIS支持就会一直无法完成Bit Lock。解决方法是把FPGA的PCIe硬核配置成支持SRISSeparate Reference Clock with Independent SSC并且把Physical Layer的接收均衡与对端匹配。另一个常见问题是极性翻转。PCIe协议允许差分对的P/N接反但需要硬件或者配置寄存器里把Polarity Inversion使能。很多FPGA IP默认不开启自动极性翻转需要在配置界面开启。如果LTSSM已经进入L0状态但数据传输错误那大概率是信号眼图问题。这时候要用示波器或误码仪测眼图。PCIe Gen3以上的信号速率到8GT/s或16GT/sPCB上一小段过孔都会造成严重损耗。如果你的板子Crosslink走线上有连接器、过孔、以及一段较长的stub建议在此处加一颗PCIe Redriver或者Retimer。Redriver只是均衡放大Retimer则重新定时效果好很多。在Crosslink设计里不要省这个器件的钱否则调试时你会怀疑人生。4.3 挡板、线缆与PCB布局经验刚才提到了半高挡板这里再多说几句。Crosslink如果不能通过背板连接器互连很可能会用PCIe线缆外接。PCIe线缆通常不是普通的SFF-8644或SFF-8639那种mini SAS线而是专用的PCIe Cable。这种线缆内部每对差分线都有屏蔽层衰减特性受长度影响极大。PCIe Gen3的x8线缆长度一般限制在1米以内超过这个长度即使有Redriver也很难稳。所以Crosslink连线尽量短。PCB布局方面Crosslink区最好放在PCB边缘远离时钟芯片和开关电源。差分线要控制100Ω±10%的阻抗对内等长误差控制在5mil以内对间等长误差根据速率来定Gen3要求小于10milGen4要求小于5mil。另外Crosslink端口的参考时钟走线也要严格走差分并且在源端串接0欧电阻或小阻值的阻尼电阻降低反射。5. 枚举后的配置与驱动交互BAR窗口设置实例5.1 设置NTB BAR和地址转换寄存器当Crosslink链路成功识别后两个RC都会各自枚举出一个NTB设备。以Linux系统为例用lspci -vvv可以看到这个设备的BAR空间。常见的NTB BAR大小是16KB到64KB里面有配置寄存器。你要做的就是通过读写寄存器来设置Memory Window。拿Xilinx NTB IP举例它的BAR是64KB对齐。里面有一组Address Translation Window寄存器每个窗口有一个基址寄存器和一个限制寄存器设置好“窗口映射到哪一段本地/远端物理地址”。比如你想把对端的1GB内存映射到本地BAR的0x200000_0000地址处就需要设置窗口使能enable bit窗口大小1GB对端物理地址例如0x100000_0000本地转换基址例如0x200000_0000硬件内部有一个TLP地址转换逻辑收到读写请求后会根据请求落在哪个窗口计算出目标物理地址。注意这个转换发生在TLP层而不是数据链路层所以不会影响CRC。如果配置错误比如窗口大小与BAR空间不匹配发起访问时会返回URUnsupported Request或CACompletion Abort软件层就会报-EPROTO之类的错误。5.2 从内核驱动和用户态看Crosslink映射在软件层Crosslink驱动通常要做两件事一是初始化NTB硬件配置窗口、中断二是提供一种让用户态程序高效发送数据的方式。Linux内核提供了ntb子系统drivers/ntb/包含ntb_transport、ntb_pingpong、ntb_tool等模块。开发时你可以在ntb_tool基础上做测试等到功能稳定后再套上自己的传输协议。用户态编程时最常用的操作是mmap映射BAR窗口。你将本地的BAR物理地址映射到用户空间虚拟地址然后直接读写指针来“访问”对端内存。不过这种方式的性能受限于CPU总线的写缓冲和BAR访问延迟。实测中单线程写1GB窗口时吞吐大概只有几个GBpsCPU占用也不低。要真正发挥Crosslink带宽必须用DMA搬运。5.3 常见问题BAR空间不足与窗口大小选择BAR空间不足是Crosslink调试中非常典型的问题。比如你的NTB设备需要映射对端8GB内存但由于BIOS给设备预留的MMIO空间可能只有2GB。这时即使硬件支持64位BAR系统也不一定能分配足够的连续地址。解决方法是在BIOS Setup中把“PCIe MMIO size”调大到64GB或更大。把Crosslink窗口设计成“分段映射”一个窗口不够就多用几个窗口每个窗口映射对端的一部分内存。如果你的CPU支持IOMMU/ACS也可以尝试动态分配工具。窗口大小也不是越大越好。窗口越大地址转换表就越大TLP首选的地址译码逻辑可能成为关键路径影响最高工作频率。对于FPGA实现的NTB尤其如此。以我实践过的经验窗口大小以2MB到64MB为单位进行配置需要大数据传输时通过滚动窗口即不断更新窗口目标地址来覆盖更大范围这是最灵活且资源占用最小的方案。6. 性能实测与优化带宽、延迟以及DMA传输路径6.1 裸传输带宽测试如何使用perftest或自研读写Crosslink的带宽测试和网络测试不一样你要测的是PCIe事务层层面的吞吐。最简单的方法是写一个“基于BAR窗口的大块DMA读/写”测试程序。做法是本地CPU将一个DMA描述符写到NTB设备的DMA控制寄存器让NTB设备或本地的DMA引擎把一大块数据从本端内存搬运到对端窗口地址然后统计时间。也可以用现成工具比如perftest里的ib_write_bw虽然针对InfiniBand但如果你用RDMA over Crosslink后者用NTB模拟RDMA也可以用。不过更接近底层的测试是直接写一个Linux内核模块调用dma_map_single、dmaengine_prep_slave_single来发起DMA传输。根据我的实测一个Gen3 x8的Crosslink链路双方都支持64位BAR且使用非阻塞DMA描述符环实测双向总带宽可以做到接近10GB/s约80Gbps。如果只是CPU直接读写BAR带宽会掉到2~3GB/s这说明硬件DMA通道不可忽略。6.2 延迟优化直接P2P DMA与经过Root Complex的比较Crosslink的低延迟优势主要体现在“直通”和“绕过Root Complex”上。当FPGA或网卡要直接向对端内存写数据时如果路径是“Endpoint A - Switch - Endpoint B”就不需要经过Host CPU或内存延迟通常在几百纳秒。而如果路径是“Endpoint A - RC A - 内存A - RC A - Crosslink - RC B - 内存B”一次传输要穿越多次PCIe桥和内存控制器延迟可能到微秒级。在Crosslink设计之初就要决定你是要“CPU访问远端内存”还是“设备直接DMA到远端”。前者绕不开本地RC延迟高后者如果用NTB的P2P DMA能力可以把延迟压到很低。Xilinx NTB IP中Endpoint的DMA引擎必须按“请求者ID”来做路由所以你需要配置好请求者ID的转换使得FPGA发起的事务能够被Crosslink正确识别并转发到对端。如果配置不当你会在对端看到一大堆收到但不知从哪来的TLP包。6.3 踩坑经验缓存一致性、MSI中断路由缓存一致性是Crosslink里最隐蔽的坑。如果你的窗口映射的是对端普通内存而且对端CPU以cacheable方式访问这块内存那么本端CPU通过BAR窗口写入的数据可能只会落到对端CPU的Cache中读的时候却读到旧值。解决方法是把窗口区域在两侧的页表里都标记为no-cache或uc。但这又会让CPU读写变慢。更高效的做法是使用DMA引擎来做搬运让DMA写事务的默认属性就是不分配Cache的同时保证两侧通过内存屏障或Doorbell来同步。MSI中断在Crosslink里同样坑多。当本端某个设备产生MSI中断MSI消息会被当作一个普通的PCIe写请求发送到MSI目标地址。在Crosslink场景下这个目标地址可能是对端CPU的某段内存而对端CPU如何得知该写请求已经到达你需要通过两个NTB的Doorbell寄存器来转发中断。我的做法是把MSI消息路由到Crosslink窗口的一个固定地址并在对端注册一个硬中断处理函数专门响应这个地址的写事件。但请注意MSI是写操作不保证顺序与其他数据写之间有严格一致性必须在数据写入完成、并刷写Write Buffer后再触发Doorbell。最后再分享一个小技巧在调试Crosslink时多用setpci直接读写NTB配置空间配合lspci -xxx看BAR和窗口寄存器的原始值。有时候驱动报错并不一定是硬件坏了而是窗口使能位没写进去。逐寄存器比对比对着代码查半天效率高得多。这套方法帮我在几个项目里快速定位了问题省下至少两天加班时间——希望你也能用上。
返回列表