资讯动态

PCIe路由机制详解:TLP地址路由、ID路由与隐式路由排查

发布时间:2026/10/26 15:09:06 来源:尧图企业网站定制
1. 先把一个TLP怎么找路这件事想明白我在调试一类板卡时遇到过这样的情况卡插上去lspci里能看到设备lspci -vv读出来的 BAR 也不是全 F但驱动一request_mem_region就报冲突ioremap之后读寄存器一律返回0xFFFFFFFF。这类问题的根源通常不在驱动本身而在PCIe 路由方式这一层——请求包TLP在交换结构里没被投递到它该去的地方或者它压根就没被发出去。这篇内容就是围绕 PCIe 的三种路由方式从 TLP 头的字段一路讲到 Linux 内核里的资源树和实际排查手段。适合谁看写过 PCI/PCIe 设备驱动、做过板级 bring-up、或者正在啃 PCIe 子系统的同学。对完全没有接触过配置空间的人前三节可能读起来有点吃力但后面几节的排查流程和踩坑记录是能直接拿去用的。我尽量把每个结论背后的为什么说清楚而不是只给操作步骤——因为路由这块知其所以然比记住命令重要得多同样一条lspci -t理解原理的人看到的是总线号分配是否合理不理解的人只看到一棵树。1.1 PCIe在软件眼里是一棵假树物理上却是一张网要理解路由先得把心理模型摆正。传统 PCI 是一条共享总线所有设备挂在同一组信号线上任何一个事务天然就能被所有设备看到谁需要谁响应仲裁器负责协调。PCIe 换了模型链路是点对点的Root Complex 往下挂 SwitchSwitch 分出多条下游链路每条链路上接 Endpoint 或者再挂 Switch。但配置软件看到的仍然是树。Switch 的每一个端口在软件眼里都是一个桥设备有自己的 Bus/Device/Function 和完整的配置头Type 1。Switch 内部那些真正的交叉开关逻辑软件是看不见的它只能通过每个端口的配置头去告诉硬件你这边的下游总线号范围是多少、内存窗口在哪、IO 窗口在哪。硬件收到一个 TLP就查这些寄存器决定往哪个口送。这个设计有个直接后果路由决策完全依赖配置头里写进去的那几个字段。软件要是把窗口写错了硬件不会报错它只会老老实实按错误的窗口去转发——发错了地方或者干脆丢包。这也是为什么 PCIe 的路由问题往往表现为静默的失败而不是一个干脆的报错。再补一个容易混淆的点PCIe-to-PCI 桥也就是常说的 PCI 桥在软件模型上和 Switch 端口没有本质区别同样是 Type 1 配置头同样有 Bus Number 和窗口寄存器。区别在于它下面挂的是真正共享的总线多个设备共享同一组地址空间和同一根中断线。所以你在lspci -t看到的那种带-[的层级有可能是 Switch也有可能是 PCI 桥需要用lspci -vv看配置头里的 Header Type 和 Capability 才能分辨。1.2 TLP头里没有目的端口只有判据很多人第一次看 TLP 格式会疑惑这 3DW/4DW 的头里哪儿写着我要去哪个设备答案是——没有这个字段。TLP 头里只有Fmt几个 DW、带不带数据、Type事务类型、Requester ID、Tag、地址或者目标 BDF 等等。至于该往哪个下游端口走是沿途每个端口根据Type决定用哪套判据、再查自己的寄存器得出的结论。三种判据对应三种路由方式路由方式判据来源典型事务谁来做决策地址路由TLP 里的 Address 字段Memory Read/Write、IO Read/Write、配置请求里的 Memory 访问每个桥比对 Base/Limit 窗口ID 路由TLP 里的 Bus/Device/Function配置请求Type 0/1、Completion每个桥比对 Primary/Secondary/Subordinate 总线号范围隐式路由TLP 里的 Message Code Route 位各类 Message中断、电源管理、错误硬件按 MsgCode 语义处理软件不参与表里第三行软件不参与这句话要理解到位隐式路由的意思是一个 Message 该往哪走是由规范预先规定好的软件既不用配窗口也不用配总线号。但这不代表软件不需要理解它——你调中断不响、PME 唤醒不了、AER 错误报不上来很多都是 Message 这一路出了问题。还有一种特殊情况值得单独提Local - Terminate at Receiver也就是 TLP 到了接收端就地消化不再转发典型的是某些厂商自定义消息和 Vendor Defined Message 的特定用法。这个在处理 Switch 内部消息或者 DPCDownstream Port Containment相关流程时会碰到普通设备驱动基本遇不上。1.3 为什么驱动作者必须关心路由有人会说枚举和资源分配不是固件和内核干的事吗我写驱动只关心 BAR 和中断不就行了。这话对一半。你确实不需要自己去写桥的窗口寄存器但你需要能判断我这个设备的 BAR 有没有被正确分配到一条能走通的路径上。举个很具体的例子。一块设备要求 8GB 的 64 位 prefetchable BAR插在一个 Switch 的 downstream port 后面。这个 Switch 上游端口的 prefetchable 窗口如果只有 4GB很常见因为固件按默认策略算的那么内核在分配的时候要么失败要么把设备挪到别的总线下去。你在驱动里pci_iomap拿到的地址看起来是个合法内核虚拟地址readl也不报错但读回来的数据全是垃圾——因为 TLP 地址落在 Switch 上游窗口之外被 URUnsupported Request丢掉了而默认的 UR 处理可能只是记个日志。所以驱动侧至少要能做三件事确认 BAR 分配结果是否落在有效窗口内、能读懂 AER 日志里报的是哪一类错误、知道从/sys和/proc/iomem的哪个文件去核对。这三件事都建立在理解路由方式的基础上。2. 地址路由内存和IO请求靠窗口一层层筛下来2.1 地址路由的TLP头长什么样一个 Memory Request 的 TLP 头Fmt字段决定了它是 3DW 还是 4DW对应 32 位地址还是 64 位地址以及带不带数据。具体编码大致是这样Fmt0b000是 3DW 无数据0b001是 3DW 带数据0b010是 4DW 无数据0b011是 4DW 带数据。注意这里的编码在不同版本规范里略有调整但低两位表示有没有数据、高位表示 DW 数这个规律是稳定的。Type字段区分是 Memory 还是 IOMemory Read/Write 的 Type 是0b00000IO Read/Write 是0b00010。地址放在头的后半部分3DW 的情况下第三个 DW 的[31:2]就是地址的[31:2]4DW 的情况下第三个 DW 是地址低 32 位第四个 DW 的[31:0]是地址高 32 位。两个细节容易忽略第一个是地址永远按 4 字节对齐编码低两位不出现。实际访问的字节范围由First DW Byte Enable和Last DW Byte Enable这 8 个 bit 决定。所以一次writeb到 BAR1产生的 TLP 里地址是 BAR 起始地址 0 或者 4按 4 字节对齐然后靠 byte enable 精确指定是哪一个字节。你在用ioremap之后调writeb时不用管这些硬件和桥会自动处理但如果你在看 trace 或者自己拼 TLP比如做验证平台这一点必须清楚。第二个是4DW 的情况下最后一个 DW 的低两位是保留的不是 byte enable。这个是很多人自己拼包时踩过的坑把 byte enable 填到那个位置对端解析出来就是错的事务。2.2 桥的Base/Limit寄存器本质上就是路由表一个 Type 1 配置头里跟地址路由相关的寄存器主要是下面这几组偏移寄存器粒度说明0x1CI/O Base / I/O Limit4KB只支持 16 位 IO 空间实际可用很小0x20Memory Base / Memory Limit1MB非预取内存窗口0x24Prefetchable Memory Base / Limit1MB支持 64 位时低 4 位固定为 0x10x28Prefetchable Base Upper 324GB仅在支持 64 位预取时有效0x2CPrefetchable Limit Upper 324GB同上0x30I/O Base / Limit Upper 1664KB扩展 IO 空间基本没人用这里的粒度是硬件写死的Memory Base 寄存器只有高 16 位有效低 20 位读出来永远是 0IO Base 只有高 12 位有效。所以一个 Memory 窗口最小 1MB、最大 4GB32 位时一个 IO 窗口最小 4KB、最大 64KB16 位时。判据是闭区间Base addr Limit两端都包含。举个具体例子Memory Base 寄存器写成0x8000、Memory Limit 写成0x80FF实际窗口就是0x80000000到0x80FFFFFF共 16MB。为什么是 16MB(0x80FF - 0x8000 1) * 1MB 0x100 * 1MB 16MB。如果你的 BAR 是 32MB这个窗口放不下硬件就不会转发TLP 直接被丢弃并上报 UR。Prefetchable Memory Base的低 4 位有个特殊编码0b0001表示这个桥支持 64 位预取窗口此时要去 0x28/0x2C 读高位。如果是0b0000说明只支持 32 位预取窗口高位寄存器无效。这个细节在写枚举代码或者做静态检查脚本时会用到。2.3 Linux里这些窗口是从哪来的这里要分两条路径讲因为 x86 和 ARM64 的做法差别不小。x86 平台走 ACPI。Root Bridge 的窗口由固件通过_CRSCurrent Resource Settings方法报告内核在drivers/acpi/pci_root.c里解析这些资源转换成pci_host_bridge的windows链表。之后pci_scan_child_bus枚举时pci_read_bridge_bases会把每个桥配置头里的 Base/Limit 读出来通过pci_bus_add_resource挂到对应pci_bus的resource[]数组上。这一步只是读不是写——也就是说如果固件已经分配好了内核就照着用。ARM64 平台通常走设备树。dma-ranges和ranges属性描述了 PCI 地址空间到 CPU 地址空间的映射关系内核在pci_host_common_probe或者各家的 host controller 驱动里解析同样生成pci_host_bridge的窗口链表。关键点在于默认情况下内核不重新分配它信任固件。只有在几种情况下才会走pci_assign_unassigned_resources()固件根本没做枚举某些嵌入式平台常见、资源冲突导致部分 BAR 没分配、或者启动参数里加了pcirealloc。重新分配的逻辑在drivers/pci/setup-bus.c里pci_bus_size_bridges先自底向上算每个桥需要多大窗口pci_bus_assign_resources再自顶向下把地址填进 Base/Limit 寄存器。这个顺序值得说一下为什么。自底向上算是因为子设备的需求决定了父窗口的最小尺寸你不可能先给父节点分配再回头改自顶向下填是因为父窗口的位置决定了子窗口能落在哪段地址范围内。这个先算后填的两遍扫描是所有资源分配算法的标准做法你在自己写 Bootloader 做简易枚举时也建议照这个思路来。2.4 地址路由失效的四种典型表现我按遇到问题的频率排一下这四种在实战中占了绝大多数。第一种读回全 F。pci_read_config_dword读 Vendor ID 得到0xFFFFFFFF。这个状态表示这个 BDF 上没有设备响应配置请求。原因可能是设备没上电、链路没训练起来、时钟没给、PERST# 没释放也可能是总线号分配错了——软件以为在 bus 3 上实际设备被枚举到了 bus 4。这种情况用lspci -t看一眼树形结构再对照lspci -vv里桥的 secondary/subordinate基本能定位。第二种UR 上报。设备在配置空间读得到但访问某个 BAR 地址时 AER 报 Unsupported Request。说明 TLP 走到了某个端口那个端口查自己的窗口发现地址不在范围内拒绝转发。这时候要去核对用户名下的 BAR 起始地址和沿途所有桥的窗口。第三种越界写导致内存踩踏。这个最危险因为不一定报错。桥的地址窗口配置错误TLP 不只没送到设备反而落到了主存上——如果窗口配成了系统 RAM 所在的地址段写操作就会直接改写内存内容内核可能莫名其妙崩溃或者数据被污染。这种情况基本只会出现在自己手写枚举代码或者固件有 bug 的场景里正常平台上不太会遇到。第四种第二块卡分不到资源。经典的插一块好使插两块就有一块不工作。原因通常是父桥的窗口不够大或者 32 位地址空间用尽。后者在有多块大显存设备或者大 BAR 网卡的机器上很常见解决办法是开 Above 4G Decoding把设备的高位 BAR 分配到 4GB 以上的地址空间去。这个在第六节还会展开。3. ID路由配置请求和Completion靠BDF定位3.1 Type 0 和 Type 1 的分水岭在哪地址路由看地址ID 路由看的是 TLP 头里的目标 BDF。这一类里最重要的是两种事务配置请求和Completion。配置请求分 Type 0 和 Type 1。区别在于Type 0 表示目标设备就在我这条总线上收到它的设备应该直接处理不再往下转发Type 1 表示目标设备在我下面某条总线上收到它的桥需要判断要不要往下转。判断逻辑很明确。一个桥收到 Type 1 配置请求取出里面携带的 Bus Number跟自己的Secondary Bus Number和Subordinate Bus Number比较如果Bus Secondary说明目标设备就挂在这个桥的下一级总线上桥把 Type 1 转换成 Type 0 发出去如果Secondary Bus Subordinate说明目标在更下面桥原样转发 Type 1如果Bus不在这个范围内桥不转发这个请求往上走或者被丢弃。这个逻辑解释了为什么总线号必须连续分配——Subordinate 是一个上界它假设下面所有总线号都在Secondary到Subordinate之间连续排列。如果枚举时分配得不连续比如某个桥下面分了 5、7 两条总线中间空着 6那 Subordinate 就得写 7桥在判断 bus 6 的时候会误以为是自己下面的但实际上那是个空号。功能上不一定出错但会让排查变复杂。内核对这一点是有约束的pci_scan_bridge_extend里分配总线号时就是按连续的方式往下发的。3.2 枚举时总线号是怎么分出来的pci_scan_bridge_extend()在drivers/pci/probe.c里逻辑大致是这样先读桥的Primary/Secondary/Subordinate三个寄存器判断这个桥下面是否已经被固件枚举过。如果Secondary是 0 或者Secondary Primary说明下面还没枚举内核就从当前总线往后分配一个新的总线号给它写进 Secondary然后递归pci_scan_child_bus去扫这条新总线。扫完之后把这条总线下面所有设备用到的最大总线号回填到 Subordinate。如果Secondary已经有效固件枚过举了内核就直接按已有的总线号范围往下扫不再重新分配。这里有个常被忽略的坑Switch 的 Upstream Port 和 Downstream Port 共享同一个物理设备的配置空间但软件看到的两个 BDF 是不同的——Upstream Port 挂在上一级总线上Downstream Port 对应一条新的总线。这就是为什么你在lspci -t里能看到 Switch 下面凭空多出一条总线的原因。如果你在做虚拟化或者自己写枚举代码这一点必须处理对否则整个下游子树都扫不到。3.3 Completion和TagID路由的反向路径PCIe 的所有非Posted事务读请求、IO 写、配置写都是拆分事务Requester 发出请求就完事Completer 稍后发一个 Completion 回来。Completion 怎么找回 Requester靠的就是 ID 路由的反向使用——Completion 头里带着Requester ID也就是发起者的 BDF和Tag。Requester ID加上Tag8 位开启 Extended Tag 后 16 位唯一标识一个未完成的事务。Requester 这边维护一张未完成事务表收到 Completion 就按这两个字段查表匹配。匹配不上就是Unexpected Completion也是一个 AER 错误类型。这套机制带来一个值得注意的约束同一个 Requester 同时能挂起的非Posted事务数量受 Tag 数量限制。8 位 Tag 最多 256 个并发16 位就是 65536。如果驱动里疯狂并发发起读请求比如用一个循环连续readl而不等结果有可能把 Tag 用完后面的请求会被阻塞。当然普通 MMIO 读是 CPU 直接发起的CPU 和 Root Complex 内部有自己的限制机制一般不会到这一步但在用 DMA 或者自己写硬件发起请求的场景下这个限制是真实存在的。还有一个相关概念是Completion Timeout。Completer 迟迟不回 Completion等够时间后 Requester 会报 Completion Timeout 错误默认行为可能是重试几次然后放弃并上报。如果设备被热拔了、或者下游链路断了就会看到这个错误。排查的时候注意区分Completion Timeout 说明请求发出去了但没回应UR 说明请求被中间某个端口拒了。两者的方向完全不同。3.4 访问配置空间的两条通道ID 路由最典型的应用场景就是读配置空间而访问配置空间在 x86 上有两条路。第一条是传统的IO 端口法往0xCF8写一个 32 位值格式大概是bit311、bit[23:16]Bus、bit[15:11]Device、bit[10:8]Function、bit[7:2]寄存器偏移然后从0xCFC读或者写数据。这条路的问题很明显每次只能访问 32 位而且必须串行——写地址、读数据这两步之间不能被打断多核并发访问需要加锁。内核里对应pci_conf1_read/write在arch/x86/pci/下。第二条是ECAMEnhanced Configuration Access Mechanism也叫 MMCONFIG把一段物理内存地址映射成配置空间地址由(Bus 20) | (Device 15) | (Function 12) | Offset算出直接readl/writel就能访问。速度快、支持并发而且能访问 4KB 的完整扩展配置空间传统的 IO 端口法每次只能到 256 字节访问扩展配置需要额外操作。ECAM 的基址来源在 x86 上是 ACPI 的MCFG表Linux 启动时pci_mmcfg_arch_init会解析这张表把每段 ECAM 地址范围注册进pci_mmcfg_list。较新的 ACPI 版本支持_CBA方法让 Root Bridge 动态给出 ECAM 基址用于支持热插拔的 host bridge。两条通道在内核里通过struct pci_ops抽象pci_bus_read_config_byte这些接口最终会调到pci_root_ops-read。ECAM 可用时优先用 ECAM不可用时回退到 IO 端口法pci_dev-cfg_size会相应变成 256 或 4096。判断当前用的是哪条路dmesg | grep -i MMCONFIG\|ECAM一般能看到。4. 隐式路由Message请求靠MsgCode自己找路4.1 没有地址也没有BDF那靠什么Message 这一类事务的特殊之处在于它既不带目标地址地址字段位置被Message Code占用了也不带目标 BDF只有一个MsgCode和一个Route编码。投递方向完全由硬件按规范约定处理。Route编码大致有这么几类语义送到 Root Complex、按地址、按 ID、从 Root Complex 向下广播、在接收端就地终止、汇聚到 Root Complex。不同版本的规范在具体比特编码上有调整比如早期版本里送到 Root Complex的编码位置和后期版本不一样所以如果你在写底层代码一定要对着手上那个版本的规范表来不要凭记忆填。软件在这里的角色不是配置路由而是理解哪类消息会走到哪然后准备好接收。这是隐式路由和另外两种最大的区别。4.2 几类必须认识的Message按使用频率排驱动作者最常打交道的是下面这几类。INTx 中断消息。Assert_INTA 到 Deassert_INTD 一共 8 个 MsgCode用于传统 PCI 中断在 PCIe 上的模拟投递。设备拉中断线时发 Assert释放时发 Deassert方向都是往 Root Complex。这类消息最终会被转换成系统中断x86 上通常是某个 IOAPIC 的 GSI或者被转成 MSI 处理这也是为什么很多设备的 INTx 明明是中断却表现为一条 Message。电源管理消息。PM_PME 用来请求唤醒PME_Turn_Off 和 PME_TO_Ack 用于电源状态切换时的握手PM_Active_State_Nak 用于 ASPM 协商。PME 这条链路是很多设备插着但系统唤不醒问题的根源。错误消息。ERR_COR、ERR_NONFATAL、ERR_FATAL 三个级别用来把设备的错误状态往 Root Complex 上报。这几个消息跟 AERAdvanced Error Reporting机制配合是排查硬件错误的主要信息源。厂商自定义消息。MsgCode 在0x7E附近的一段是留给厂商的实际用途包括 DPC 触发、LTRLatency Tolerance Reporting消息、ATS Invalidation 等等。这些通常不需要驱动直接处理由内核的对应子系统接管。4.3 Root Complex收到消息之后交给谁这一步是隐式路由的最终落点也是很多问题暴露的地方。INTx 消息在 Root Complex 上被转换成中断控制器的一个输入内核这边就是普通的request_irq。如果你的驱动用pci_alloc_irq_vectors拿了 MSI/MSI-X就完全不走这条路但如果设备只支持 INTx或者 MSI 分配失败回退了那中断路径上就一定要保证 INTx 消息能正确到达。PME 消息的处理在drivers/pci/pcie/pme.c里pcie_pme_work_fn会遍历下游设备、读 PME Status 位、调对应的回调。如果你写过带唤醒功能的驱动pci_enable_wake和device_set_wakeup_enable设置的东西最终就是在这一层生效的。AER 错误消息的处理在drivers/pci/pcie/aer.caer_isr收到中断后遍历各设备读错误状态寄存器然后分类上报。这一路的信息在dmesg里就是形如pcieport 0000:00:01.0: AER: Corrected error received这样的日志。看日志的时候重点看Corrected、Non-Fatal、Fatal三个级别以及后面的TLP Header打印——那里面直接给出了出错事务的地址或者 BDF是定位路由问题最直接的证据。5. 路由出问题时按这个顺序往下查5.1 第一步永远是分清看不见设备和看见了但走不通这两类问题的排查方向完全不同混在一起查会浪费大量时间。判断方法很直接lspci -s BDF能不能列出来。列不出来就是枚举阶段的问题往链路训练、电源、时钟、复位方向查能列出来但访问异常才是路由或者资源分配的问题。如果设备不在预期的 BDF 上用lspci -t看整棵树的形状再对照lspci -vv里每个桥的Bus: primaryXX, secondaryYY, subordinateZZ三个值看总线号分配是否符合预期。我遇到过一次是某个桥的 Subordinate 被固件写成了 0导致内核认为它下面没有总线整个下游子树都没扫——lspci -t上看就是那个桥后面光秃秃的。5.2 用/sys和/proc核对窗口是否真的覆盖住了BAR设备能列出来之后下一步核对地址窗口。三个地方要看/sys/bus/pci/devices/0000:xx:yy.z/resource文件里每一行是一个 BAR格式是start end flagsflags里的0x...200表示 memory、0x...400表示 prefetchable、0x...100表示 IO。/proc/iomem能看到整条 PCI 总线占用的地址范围和每一段被哪个设备 claim 了。如果某个 BAR 的起始地址在/proc/iomem里找不到对应的 PCI Bus 段说明它落在了一个没人管的地址区间TLP 发出去必然被拒。桥的窗口用setpci直接读配置头最直观。比如读 00:01.0 的 Memory Base/Limitsetpci -s 00:01.0 20.l输出一个 32 位值低 16 位是 Base高 16 位有效需要左移 16高 16 位是 Limit同样左移 16 再或上0xFFFFF。IO 窗口对应偏移0x1cprefetchable 对应0x24高位对应0x28和0x2c。把设备 BAR 的起始地址和大小跟沿途所有桥的窗口逐一比对只要有任何一个桥的窗口不包含它路由就是断的。这个比对过程手动做比较枯燥我一般写个小脚本遍历/sys/bus/pci/devices/*/把每个设备的 BAR 和它父总线的窗口打出来对照。5.3 AER日志里藏着出错TLP的原始信息如果设备能访问一部分地址另一部分报错那就去看 AER 日志。dmesg | grep -iE aer|corrected|unsupportedUnsupported Request表示某个端口拒了这个 TLP通常就是窗口不覆盖。Completer Abort是设备自己拒了可能地址不在设备的 BAR 范围内。Malformed TLP是包头格式有问题自己拼包的场景下才会遇到。Completion Timeout上面说过了是没回应。AER 日志里那段TLP Header十六进制 dump 非常有价值可以解析出Fmt、Type、Requester ID、Tag、地址。我习惯把这几个字段解出来然后拿地址去跟窗口比、拿 Requester ID 去确认是哪个设备发起的基本上两个字段就能把范围缩小到具体某条链路。5.4 两个实际排查记录案例一Switch 后面挂多块卡第二块 BAR 分配失败。现象是lspci能看到两块卡但第二块的 BAR 全是 0/sys/.../resource里显示start0 end0。查 Switch 上游端口的 prefetchable 窗口只有 256MB而两块卡各要 128MB prefetchable加上对齐要求之后第二块放不下了。处理思路是让内核重新分配加pcirealloc启动参数让pci_assign_unassigned_resources重新算窗口。但这样做之后发现上游端口的 prefetchable 窗口最大值受限于它上一级Root Port给出的范围还是不够。最终的解决办法是在固件阶段就把这个 Root Port 的 prefetchable 窗口配大或者在设备树里把ranges的范围放宽。这件事让我记住一条资源不足的时候往上找一层问题往往在更上游的桥。案例二64 位 BAR 落在了 CPU 访问不到的地址段。现象是驱动ioremap成功、readl不报错但读回来的全是0xFFFFFFFF。查/proc/iomem发现这个 BAR 的地址落在PCI Bus 0000:00这一大段里看起来正常。再看 Root Bridge 的窗口发现它只覆盖了低 4GB 的一部分而这块设备的 64 位 BAR 被分到了 4GB 以上——地址在 PCI 域里是合法的但 Root Complex 没有把它映射到 CPU 可访问的地址空间。这类问题在 ARM64 平台上尤其容易出现因为dma-ranges的映射关系决定了哪些 PCI 地址能真正到达 CPU。x86 上有 Above 4G Decoding 开关也是同样道理。解决办法不是改驱动而是把平台的地址映射范围配全。6. 写驱动时跟路由相关的几处坑6.1 BAR尺寸和对齐不是你想要多大就多大设备的 BAR 大小由硬件决定软件只能读不能改。探测方法是往 BAR 写全 1再读回来读到的值取反加一就是大小。这个操作有风险——它会覆盖 BAR 原来的值所以必须先把原值存下来探测完写回去而且要确保设备此时没有被驱动使用。pci_resource_len()拿到的是内核探测后记录的大小这个值在枚举阶段就已经算好了驱动直接用就行不要自己去写全 1 探测。我见过有人在probe里自己写全 1结果把固件已经分配好的地址搞坏了后续所有访问全错。对齐方面BAR 大小是 2 的幂起始地址必须按大小对齐。这个约束是硬件保证的你在设计设备端的时候就要考虑好别在 FPGA 上搞一个 3MB 的 BAR——它会被向上取整成 4MB白白浪费地址空间。6.2 Prefetchable和non-prefetchable不能乱标BAR 的flags里那个 prefetchable 位含义是这块内存被读的时候没有副作用可以预取、可以合并、可以乱序。很多人在 FPGA 上做设备的时候图省事把控制寄存器区也标成 prefetchable结果就是 CPU 的预取逻辑会去读那些读一下就会清零的寄存器状态全乱。判断标准很清晰只要是带读清零、读递增指针、FIFO 弹出这类副作用的寄存器就必须标成 non-prefetchable。只有纯粹的存储区比如设备上的 DDR、大块数据缓冲才标 prefetchable。这个标志标错了不会立刻报错而是表现为偶发数据错乱排查起来非常痛苦。我在一个采集卡项目上就踩过这个坑最后是靠抓 PCIe 分析仪才定位到的。6.3 IO BAR尽量别用IO 空间的总量在 x86 上非常有限16 位寻址只有 64KB扩展之后也就 64KB 到 4GB 之间一小段而且很多平台根本不给分配。新设计设备的时候一律用 Memory BARIO BAR 只在兼容老设备的时候才考虑。如果确实遇到了只有 IO BAR 的设备注意 IO 窗口的粒度是 4KB而 IO BAR 最小可以到 4 字节所以对齐上会很浪费。另外访问 IO 空间要用inb/outb这一套内核里是inb()、outb()或者ioremap之后用readb不能用普通的readl/writel去访问内存映射的地址。6.4 上游桥窗口不够时先看能不能挪再考虑重分配设备 BAR 分配不上的时候处理顺序建议是这样先看能不能用resource_alignment参数调整对齐和大小让内核把它挪到一个更合适的位置pciresource_alignment00:02.0;8M这个参数可以在不改硬件设计的前提下解决一部分位置不合适的问题。不行的话再考虑pcirealloc让内核重新分配整个树。要注意这个操作会改动固件已经配好的东西在某些平台上可能引发其他设备异常属于有风险的操作生产环境慎用。最后才考虑改固件或者改硬件设计。从工程角度看最好是在设计阶段就把窗口需求算清楚别等到调试阶段才发现不够。6.5 DMA和路由是两回事但经常一起出问题地址路由管的是 CPU 访问设备MMIO这条方向DMA 是设备访问内存的反方向。两者用的机制不一样MMIO 路由靠桥的窗口寄存器DMA 靠设备发出的 Memory Write TLP 里的目标地址那个地址由驱动通过dma_map_*系列接口拿到。如果平台上有 IOMMUDMA 地址还要经过一层地址转换这时候驱动拿到的dma_addr_t和设备实际写出去的 PCI 地址不是同一个值。这种情况下如果出现 DMA 数据不对先确认dma_map返回的地址和 IOMMU 的映射窗口是否匹配别一头扎进 PCIe 路由里查——方向不对。判断方法很简单MMIO 读写正常但 DMA 数据错问题在 DMA 这一侧MMIO 读写就不正常才往路由方向查。这个二分法能省掉大量时间。7. 最后说几句我在实际项目里的体会PCIe 路由这三块内容刚接触的时候最容易犯的错是只记结论不记判据。比如记住了配置请求看 BDF但没搞清楚 Type 0 和 Type 1 的转换时机遇到 Switch 后面的设备枚举不出来就完全没思路。真正管用的做法是拿一张纸把自己平台的拓扑画出来每个桥的 Primary/Secondary/Subordinate、每组窗口的 Base/Limit 都标上去然后拿一个具体的访问地址模拟一遍它从 CPU 出发经过哪些端口、每个端口怎么判断。画过两三次之后这套逻辑就变成条件反射了。另外一个很实用的习惯是改任何跟资源分配相关的启动参数之前先把当前的/proc/iomem和所有桥的配置头 dump 一份存下来。这类问题经常是改了之后现象变化了但说不清是变好还是变坏有基线对比就清楚多了。我做板级调试的时候会写一个小脚本一次性把lspci -vvv、/proc/iomem、/sys/bus/pci/devices/*/resource全抓下来存成带时间戳的目录出问题的时候直接 diff 两个状态比人肉回忆靠谱得多。还有一点是关于隐式路由的这类问题最容易被忽略因为它不体现在任何寄存器的配置上。中断不响、唤醒失效、错误报不上来很多人第一反应是查驱动其实应该先看dmesg里有没有 AER 或者 PME 相关的记录确认消息这一路是不是通的。把这条链路当成一个独立的排查维度而不是附属于中断子系统思路会清晰很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑