英文

1. 如何编写 Linux PCI 驱动程序

作者:

PCI 的世界广阔且充满(大多是不愉快的)惊喜。由于每个 CPU 架构都实现了不同的芯片组,并且 PCI 设备有不同的要求(嗯,“功能”),因此 Linux 内核中的 PCI 支持并不像人们希望的那样简单。这篇简短的论文试图向所有潜在的驱动程序作者介绍 Linux 用于 PCI 设备驱动程序的 API。

更完整的资源是 Jonathan Corbet、Alessandro Rubini 和 Greg Kroah-Hartman 撰写的第三版“Linux 设备驱动程序”。 LDD3 可从以下网址免费获得(在知识共享许可下):https://lwn.net/Kernel/LDD3/

但是,请记住,所有文档都可能存在“位腐烂”的问题。如果事情没有按照此处描述的方式工作,请参考源代码。

请将有关 Linux PCI API 的问题/评论/补丁发送到“Linux PCI”<linux-pci@atrey.karlin.mff.cuni.cz>邮件列表。

1.1. PCI 驱动程序的结构

PCI 驱动程序通过 pci_register_driver() “发现”系统中的 PCI 设备。实际上,顺序是相反的。当 PCI 通用代码发现新设备时,会通知具有匹配“描述”的驱动程序。有关详细信息,请参见下文。

pci_register_driver() 将大部分设备探测工作留给 PCI 层,并支持设备的在线插入/移除 [因此在单个驱动程序中支持热插拔 PCI、CardBus 和 Express-Card]。 pci_register_driver() 调用需要传入函数指针表,从而决定驱动程序的高级结构。

一旦驱动程序知道 PCI 设备并获得所有权,驱动程序通常需要执行以下初始化

  • 启用设备

  • 请求 MMIO/IOP 资源

  • 设置 DMA 掩码大小(用于相干和流式 DMA)

  • 分配和初始化共享控制数据 (pci_allocate_coherent())

  • 访问设备配置空间(如果需要)

  • 注册 IRQ 处理程序 (request_irq())

  • 初始化非 PCI(即芯片的 LAN/SCSI/等部分)

  • 启用 DMA/处理引擎

当完成设备的使用,并且可能需要卸载模块时,驱动程序需要执行以下步骤

  • 禁用设备生成 IRQ

  • 释放 IRQ (free_irq())

  • 停止所有 DMA 活动

  • 释放 DMA 缓冲区(流式和相干)

  • 从其他子系统取消注册(例如,scsi 或 netdev)

  • 释放 MMIO/IOP 资源

  • 禁用设备

以下部分涵盖了大部分这些主题。其余部分请参阅 LDD3 或 <linux/pci.h>。

如果未配置 PCI 子系统(未设置 CONFIG_PCI),则下面描述的大多数 PCI 函数都定义为内联函数,要么完全为空,要么只是返回适当的错误代码,以避免驱动程序中出现大量 ifdef。

1.2. pci_register_driver() 调用

PCI 设备驱动程序在其初始化期间使用指向描述驱动程序的结构的指针 (struct pci_driver) 调用 pci_register_driver()

struct pci_driver

PCI 驱动程序结构

定义:

struct pci_driver {
    const char              *name;
    const struct pci_device_id *id_table;
    int (*probe)(struct pci_dev *dev, const struct pci_device_id *id);
    void (*remove)(struct pci_dev *dev);
    int (*suspend)(struct pci_dev *dev, pm_message_t state);
    int (*resume)(struct pci_dev *dev);
    void (*shutdown)(struct pci_dev *dev);
    int (*sriov_configure)(struct pci_dev *dev, int num_vfs);
    int (*sriov_set_msix_vec_count)(struct pci_dev *vf, int msix_vec_count);
    u32 (*sriov_get_vf_total_msix)(struct pci_dev *pf);
    const struct pci_error_handlers *err_handler;
    const struct attribute_group **groups;
    const struct attribute_group **dev_groups;
    struct device_driver    driver;
    struct pci_dynids       dynids;
    bool driver_managed_dma;
};

成员

name

驱动程序名称。

id_table

指向驱动程序感兴趣的设备 ID 表的指针。大多数驱动程序应使用 MODULE_DEVICE_TABLE(pci,...) 导出此表。

probe

对于与 ID 表匹配且尚未被其他驱动程序“拥有”的所有 PCI 设备,会调用此探测函数(在执行 pci_register_driver() 时用于已存在的设备,或者稍后在新设备插入时)。此函数会传递一个“struct pci_dev *”,用于其 ID 表中的条目与设备匹配的每个设备。当驱动程序选择“拥有”设备时,探测函数返回零,否则返回错误代码(负数)。探测函数始终从进程上下文调用,因此它可以休眠。

remove

每当移除此驱动程序正在处理的设备时(在注销驱动程序期间或手动将其从热插拔插槽中拔出时),都会调用 remove() 函数。 remove 函数始终从进程上下文调用,因此它可以休眠。

suspend

将设备置于低功耗状态。

resume

将设备从低功耗状态唤醒。(有关 PCI 电源管理和相关功能的说明,请参阅 PCI 电源管理。)

shutdown

挂钩到 reboot_notifier_list (kernel/sys.c)。旨在停止任何空闲的 DMA 操作。对于启用网络唤醒 (NIC) 或在重新启动之前更改设备的电源状态很有用。例如,drivers/net/e100.c。

sriov_configure

可选的驱动程序回调,允许通过 sysfs “sriov_numvfs” 文件配置要启用的 VF 数量。

sriov_set_msix_vec_count

PF 驱动程序回调以更改 VF 上的 MSI-X 向量数。通过 sysfs “sriov_vf_msix_count” 触发。这将更改 VF 消息控制寄存器中的 MSI-X 表大小。

sriov_get_vf_total_msix

PF 驱动程序回调以获取可用于分配给 VF 的 MSI-X 向量总数。

err_handler

请参阅 PCI 错误恢复

groups

Sysfs 属性组。

dev_groups

附加到设备的属性,一旦绑定到驱动程序就会创建这些属性。

driver

驱动程序模型结构。

dynids

动态添加的设备 ID 列表。

driver_managed_dma

设备驱动程序不使用内核 DMA API 进行 DMA。对于大多数设备驱动程序,只要所有 DMA 都通过内核 DMA API 处理,就不需要关心此标志。对于一些特殊的驱动程序,例如 VFIO 驱动程序,它们知道如何自行管理 DMA 并设置此标志,以便 IOMMU 层允许它们设置和管理自己的 I/O 地址空间。

ID 表是一个 struct pci_device_id 条目数组,以全零条目结尾。通常首选使用 static const 定义。

struct pci_device_id

PCI 设备 ID 结构

定义:

struct pci_device_id {
    __u32 vendor, device;
    __u32 subvendor, subdevice;
    __u32 class, class_mask;
    kernel_ulong_t driver_data;
    __u32 override_only;
};

成员

vendor

要匹配的供应商 ID(或 PCI_ANY_ID)

device

要匹配的设备 ID(或 PCI_ANY_ID)

subvendor

要匹配的子系统供应商 ID(或 PCI_ANY_ID)

subdevice

要匹配的子系统设备 ID(或 PCI_ANY_ID)

class

要匹配的设备类、子类和“接口”。有关类的完整列表,请参见 PCI 本地总线规范的附录 D 或 include/linux/pci_ids.h。大多数驱动程序不需要指定类/class_mask,因为供应商/设备通常足够。

class_mask

限制比较类字段的哪些子字段。有关用法示例,请参见 drivers/scsi/sym53c8xx_2/。

driver_data

驱动程序私有的数据。大多数驱动程序不需要使用 driver_data 字段。最佳实践是将 driver_data 用作等效设备类型静态列表的索引,而不是将其用作指针。

override_only

仅当 dev->driver_override 是此驱动程序时才匹配。

大多数驱动程序只需要 PCI_DEVICE()PCI_DEVICE_CLASS() 来设置 pci_device_id 表。

新的 PCI ID 可以在运行时添加到设备驱动程序 pci_ids 表中,如下所示

echo "vendor device subvendor subdevice class class_mask driver_data" > \
/sys/bus/pci/drivers/{driver}/new_id

所有字段都作为十六进制值传入(没有前导 0x)。 vendor 和 device 字段是必需的,其他字段是可选的。用户只需要传递尽可能多的可选字段即可

  • subvendor 和 subdevice 字段默认为 PCI_ANY_ID (FFFFFFFF)

  • class 和 classmask 字段默认为 0

  • driver_data 默认为 0UL。

  • override_only 字段默认为 0。

请注意,driver_data 必须与驱动程序中定义的任何 pci_device_id 条目使用的值匹配。如果所有 pci_device_id 条目都有一个非零的 driver_data 值,则此 driver_data 字段是必需的。

添加后,对于其(新更新的)pci_ids 列表中列出的任何未声明的 PCI 设备,将调用驱动程序探测例程。

当驱动程序退出时,它只调用 pci_unregister_driver(),并且 PCI 层会自动调用驱动程序处理的所有设备的 remove 钩子。

1.2.1. 驱动程序函数/数据的“属性”

请酌情标记初始化和清理函数(相应的宏在 <linux/init.h> 中定义)

__init

初始化代码。在驱动程序初始化后被丢弃。

__exit

退出代码。对于非模块化驱动程序,将被忽略。

关于何时/何地使用上述属性的提示
  • module_init()/module_exit() 函数(以及仅从这些函数调用的所有初始化函数)应标记为 __init/__exit。

  • 不要标记 struct pci_driver

  • 如果您不确定要使用哪个标记,请不要标记函数。最好不要标记函数,也不要标记错误的函数。

1.3. 如何手动查找 PCI 设备

PCI 驱动程序应该有充分的理由不使用 pci_register_driver() 接口来搜索 PCI 设备。PCI 设备由多个驱动程序控制的主要原因是,一个 PCI 设备实现了几种不同的硬件服务。例如,组合的串行/并行端口/软盘控制器。

可以使用以下构造执行手动搜索

按供应商和设备 ID 搜索

struct pci_dev *dev = NULL;
while (dev = pci_get_device(VENDOR_ID, DEVICE_ID, dev))
        configure_device(dev);

按类 ID 搜索(以类似方式迭代)

pci_get_class(CLASS_ID, dev)

按供应商/设备和子系统供应商/设备 ID 搜索

pci_get_subsys(VENDOR_ID,DEVICE_ID, SUBSYS_VENDOR_ID, SUBSYS_DEVICE_ID, dev).

您可以使用常量 PCI_ANY_ID 作为 VENDOR_ID 或 DEVICE_ID 的通配符替换。例如,这允许搜索来自特定供应商的任何设备。

这些函数是热插拔安全的。它们会增加它们返回的 pci_dev 的引用计数。您最终(可能在模块卸载时)必须通过调用 pci_dev_put() 来减少这些设备的引用计数。

1.4. 设备初始化步骤

正如引言中所述,大多数 PCI 驱动程序需要以下步骤进行设备初始化

  • 启用设备

  • 请求 MMIO/IOP 资源

  • 设置 DMA 掩码大小(用于相干和流式 DMA)

  • 分配和初始化共享控制数据 (pci_allocate_coherent())

  • 访问设备配置空间(如果需要)

  • 注册 IRQ 处理程序 (request_irq())

  • 初始化非 PCI(即芯片的 LAN/SCSI/等部分)

  • 启用 DMA/处理引擎。

驱动程序可以随时访问 PCI 配置空间寄存器。(好吧,几乎。当运行 BIST 时,配置空间可能会消失...但这只会导致 PCI 总线主控中止,并且配置读取将返回垃圾)。

1.4.1. 启用 PCI 设备

在触及任何设备寄存器之前,驱动程序需要通过调用 pci_enable_device() 来启用 PCI 设备。这将

  • 如果设备处于挂起状态,则唤醒设备,

  • 分配设备的 I/O 和内存区域(如果 BIOS 没有这样做),

  • 分配 IRQ(如果 BIOS 没有这样做)。

注意

pci_enable_device() 可能会失败!检查返回值。

警告

操作系统错误:我们不会在启用这些资源之前检查资源分配。如果我们先调用 pci_request_resources(),然后再调用 pci_enable_device(),则该顺序更有意义。目前,当两个设备分配到同一范围时,设备驱动程序无法检测到该错误。这不是一个常见问题,并且不太可能很快得到修复。

之前已经讨论过这个问题,但在 2.6.19 版本中尚未更改:https://lore.kernel.org/r/[email protected]/

pci_set_master() 将通过设置 PCI_COMMAND 寄存器中的总线主控位来启用 DMA。如果 BIOS 将延迟计时器值设置为错误的值,它也会修复该值。pci_clear_master() 将通过清除总线主控位来禁用 DMA。

如果 PCI 设备可以使用 PCI 内存写入无效事务,请调用 pci_set_mwi()。这将启用用于 Mem-Wr-Inval 的 PCI_COMMAND 位,并确保正确设置缓存行大小寄存器。检查 pci_set_mwi() 的返回值,因为并非所有架构或芯片组都可能支持内存写入无效。或者,如果 Mem-Wr-Inval 可以有但不是必需的,请调用 pci_try_set_mwi(),以让系统尽力启用 Mem-Wr-Inval。

1.4.2. 请求 MMIO/IOP 资源

不应直接从 PCI 设备配置空间读取内存 (MMIO) 和 I/O 端口地址。使用 pci_dev 结构中的值,因为 PCI“总线地址”可能已被 arch/芯片组特定的内核支持重新映射为“主机物理”地址。

有关如何访问设备寄存器或设备内存,请参阅 io_mapping 函数

设备驱动程序需要调用 pci_request_region() 来验证没有其他设备正在使用相同的地址资源。相反,驱动程序应在调用 pci_disable_device() 后调用 pci_release_region()。其目的是防止两个设备在同一地址范围内发生冲突。

提示

请参阅上面的操作系统错误注释。当前 (2.6.19),驱动程序只能在调用 pci_enable_device() 之后才能确定 MMIO 和 IO 端口资源的可用性。

pci_request_region() 的通用版本是 request_mem_region()(用于 MMIO 范围)和 request_region()(用于 IO 端口范围)。将它们用于未通过“普通”PCI BAR 描述的地址资源。

另请参阅下面的 pci_request_selected_regions()

1.4.3. 设置 DMA 掩码大小

注意

如果以下任何内容没有意义,请参阅 使用通用设备的动态 DMA 映射。本节只是提醒驱动程序需要指示设备的 DMA 功能,而不是 DMA 接口的权威来源。

虽然所有驱动程序都应明确指示 PCI 总线主控的 DMA 功能(例如,32 位或 64 位),但对于流式传输数据具有超过 32 位总线主控功能的设备,需要驱动程序通过使用适当的参数调用 dma_set_mask() 来“注册”此功能。一般来说,这允许在系统 RAM 存在于 4G _物理_地址之上的系统上进行更高效的 DMA。

所有符合 PCI-X 和 PCIe 标准的设备的驱动程序都必须调用 dma_set_mask(),因为它们是 64 位 DMA 设备。

类似地,如果设备可以通过调用 dma_set_coherent_mask() 直接寻址系统 RAM 中 4G 物理地址之上的“一致内存”,则驱动程序还必须“注册”此功能。同样,这包括所有符合 PCI-X 和 PCIe 标准的设备的驱动程序。许多 64 位“PCI”设备(在 PCI-X 之前)和一些 PCI-X 设备都支持用于有效载荷(“流式传输”)数据的 64 位 DMA,但不支持控制(“一致”)数据。

1.4.4. 设置共享控制数据

设置 DMA 掩码后,驱动程序可以分配“一致”(又名共享)内存。有关 DMA API 的完整说明,请参阅 使用通用设备的动态 DMA 映射。本节只是提醒您需要在启用设备上的 DMA 之前完成此操作。

1.4.5. 初始化设备寄存器

某些驱动程序需要编程特定的“功能”字段或初始化或重置其他“供应商特定”的寄存器。例如,清除挂起的interrupt。

1.4.6. 注册 IRQ 处理程序

虽然此处描述的最后一步是调用 request_irq(),但通常这只是初始化设备的另一个中间步骤。此步骤通常可以延迟到设备打开以供使用。

IRQ 线的所有中断处理程序都应使用 IRQF_SHARED 注册,并使用 devid 将 IRQ 映射到设备(请记住,所有 PCI IRQ 线都可以共享)。

request_irq() 将中断处理程序和设备句柄与中断号相关联。从历史上看,中断号代表从 PCI 设备运行到中断控制器的 IRQ 线。使用 MSI 和 MSI-X(更多如下),中断号是 CPU“向量”。

request_irq() 也会启用中断。在注册中断处理程序之前,请确保设备处于静止状态,并且没有挂起的中断。

MSI 和 MSI-X 是 PCI 功能。两者都是“消息信号中断”,它们通过 DMA 写入本地 APIC 将中断传递到 CPU。MSI 和 MSI-X 之间的根本区别在于如何分配多个“向量”。MSI 需要连续的向量块,而 MSI-X 可以分配几个单独的向量。

通过在调用 pci_alloc_irq_vectors() 时使用 PCI_IRQ_MSI 和/或 PCI_IRQ_MSIX 标志,然后在调用 request_irq() 之前,可以启用 MSI 功能。这将导致 PCI 支持将 CPU 向量数据编程到 PCI 设备的功能寄存器中。许多架构、芯片组或 BIOS 不支持 MSI 或 MSI-X,因此仅使用 PCI_IRQ_MSI 和 PCI_IRQ_MSIX 标志调用 pci_alloc_irq_vectors 将会失败,所以请始终同时指定 PCI_IRQ_INTX。

对于 MSI/MSI-X 和传统的 INTx 有不同中断处理程序的驱动程序,应该在调用 pci_alloc_irq_vectors 后,根据 pci_dev 结构中的 msi_enabled 和 msix_enabled 标志选择正确的处理程序。

使用 MSI 有(至少)两个非常好的理由:

  1. MSI 从定义上来说是一个独占的中断向量。这意味着中断处理程序无需验证是否是其设备引起了中断。

  2. MSI 避免了 DMA/IRQ 竞争条件。当 MSI 被传递时,保证 DMA 到主机内存对主机 CPU 可见。这对于数据一致性和避免过时的控制数据非常重要。此保证允许驱动程序省略 MMIO 读取以刷新 DMA 流。

有关 MSI/MSI-X 用法的示例,请参阅 drivers/infiniband/hw/mthca/ 或 drivers/net/tg3.c。

1.5. PCI 设备关闭

当 PCI 设备驱动程序被卸载时,需要执行以下大部分步骤:

  • 禁用设备生成 IRQ

  • 释放 IRQ (free_irq())

  • 停止所有 DMA 活动

  • 释放 DMA 缓冲区(流式和相干)

  • 从其他子系统取消注册(例如,scsi 或 netdev)

  • 禁止设备响应 MMIO/IO 端口地址

  • 释放 MMIO/IO 端口资源

1.5.1. 停止设备上的 IRQ

如何执行此操作是特定于芯片/设备的。如果不这样做,如果(并且只有当)IRQ 与另一个设备共享,则可能会出现“尖叫中断”。

当共享 IRQ 处理程序被“取消挂钩”时,仍然需要启用使用同一 IRQ 线路的剩余设备。因此,如果“取消挂钩”的设备断言 IRQ 线,系统会假设是剩余设备之一断言了 IRQ 线。由于其他设备都不会处理 IRQ,系统将“挂起”,直到它确定 IRQ 不会被处理并屏蔽 IRQ(100,000 次迭代后)。一旦共享 IRQ 被屏蔽,剩余的设备将停止正常运行。这不是一个好情况。

这是在可用时使用 MSI 或 MSI-X 的另一个原因。MSI 和 MSI-X 被定义为独占中断,因此不易受“尖叫中断”问题的影响。

1.5.2. 释放 IRQ

一旦设备静止(不再有 IRQ),可以调用 free_irq()。此函数将在处理完任何挂起的 IRQ 后返回控制权,从该 IRQ 中“取消挂钩”驱动程序的 IRQ 处理程序,并且如果没人使用该 IRQ,最终释放该 IRQ。

1.5.3. 停止所有 DMA 活动

在尝试取消分配 DMA 控制数据之前停止所有 DMA 操作至关重要。不这样做会导致内存损坏、挂起,并且在某些芯片组上会导致硬崩溃。

在停止 IRQ 后停止 DMA 可以避免 IRQ 处理程序可能重新启动 DMA 引擎的竞争。

虽然此步骤听起来很明显且很简单,但过去一些“成熟”的驱动程序并没有正确执行此步骤。

1.5.4. 释放 DMA 缓冲区

一旦 DMA 停止,首先清理流式 DMA。即,取消映射数据缓冲区,并将缓冲区返回给“上游”所有者(如果有)。

然后清理包含控制数据的“一致性”缓冲区。

有关取消映射接口的详细信息,请参阅 使用通用设备的动态 DMA 映射

1.5.5. 从其他子系统中注销

大多数低级 PCI 设备驱动程序支持一些其他子系统,如 USB、ALSA、SCSI、NetDev、Infiniband 等。请确保您的驱动程序没有丢失来自其他子系统的资源。如果发生这种情况,通常的症状是当子系统尝试调用已卸载的驱动程序时出现 Oops(panic)。

1.5.6. 禁止设备响应 MMIO/IO 端口地址

io_unmap() MMIO 或 IO 端口资源,然后调用 pci_disable_device()。这是 pci_enable_device() 的对称反操作。在调用 pci_disable_device() 后,不要访问设备寄存器。

1.5.7. 释放 MMIO/IO 端口资源

调用 pci_release_region() 将 MMIO 或 IO 端口范围标记为可用。不这样做通常会导致无法重新加载驱动程序。

1.6. 如何访问 PCI 配置空间

您可以使用 pci_(read|write)_config_(byte|word|dword) 来访问由 struct pci_dev * 表示的设备的配置空间。所有这些函数在成功时返回 0,或者返回一个错误代码 (PCIBIOS_...),该错误代码可以通过 pcibios_strerror 转换为文本字符串。大多数驱动程序期望访问有效的 PCI 设备不会失败。

如果您没有可用的 struct pci_dev,您可以调用 pci_bus_(read|write)_config_(byte|word|dword) 来访问该总线上给定的设备和功能。

如果您访问配置标头的标准部分中的字段,请使用 <linux/pci.h> 中声明的位置和位的符号名称。

如果您需要访问扩展 PCI 功能寄存器,只需调用 pci_find_capability() 来查找特定的功能,它将为您找到相应的寄存器块。

1.7. 其他有趣的函数

pci_get_domain_bus_and_slot()

查找与给定域、总线和插槽号对应的 pci_dev。如果找到设备,其引用计数会增加。

pci_set_power_state()

设置 PCI 电源管理状态(0=D0 ... 3=D3)

pci_find_capability()

在设备的功能列表中查找指定的功能。

pci_resource_start()

返回给定 PCI 区域的总线起始地址

pci_resource_end()

返回给定 PCI 区域的总线结束地址

pci_resource_len()

返回 PCI 区域的字节长度

pci_set_drvdata()

为 pci_dev 设置私有驱动程序数据指针

pci_get_drvdata()

为 pci_dev 返回私有驱动程序数据指针

pci_set_mwi()

启用内存写入无效事务。

pci_clear_mwi()

禁用内存写入无效事务。

1.8. 杂项提示

当向用户显示 PCI 设备名称时(例如,当驱动程序想告诉用户它找到了什么卡时),请使用 pci_name(pci_dev)。

始终通过指向 pci_dev 结构的指针来引用 PCI 设备。所有 PCI 层函数都使用此标识,并且它是唯一合理的标识。除了非常特殊的用途外,不要使用总线/插槽/功能号 - 在具有多个主总线的系统上,它们的语义可能非常复杂。

不要尝试在您的驱动程序中启用快速背靠背写入。总线上所有设备都需要能够执行此操作,因此这需要由平台和通用代码处理,而不是由单独的驱动程序处理。

1.9. 供应商和设备标识

除非它们在多个驱动程序之间共享,否则不要将新的设备或供应商 ID 添加到 include/linux/pci_ids.h。如果它们有用,您可以在驱动程序中添加私有定义,或者只使用普通的十六进制常量。

设备 ID 是任意十六进制数(由供应商控制),通常只在一个位置使用,即 pci_device_id 表。

请将新的供应商/设备 ID 提交到 https://pci-ids.ucw.cz/。在 https://github.com/pciutils/pciids 上有 pci.ids 文件的镜像。

1.10. 过时的函数

当尝试将旧驱动程序移植到新的 PCI 接口时,您可能会遇到一些函数。它们不再出现在内核中,因为它们与热插拔或 PCI 域或具有健全的锁定不兼容。

pci_find_device()

已由 pci_get_device() 取代

pci_find_subsys()

已由 pci_get_subsys() 取代

pci_find_slot()

已由 pci_get_domain_bus_and_slot() 取代

pci_get_slot()

已由 pci_get_domain_bus_and_slot() 取代

替代方法是传统的 PCI 设备驱动程序,它会遍历 PCI 设备列表。这仍然是可能的,但不建议这样做。

1.11. MMIO 空间和“写入发布”

将驱动程序从使用 I/O 端口空间转换为使用 MMIO 空间通常需要进行一些额外的更改。特别是,需要处理“写入后发”。许多驱动程序(例如 tg3、acenic、sym53c8xx_2)已经这样做了。I/O 端口空间保证写事务在 CPU 继续执行之前到达 PCI 设备。写入 MMIO 空间允许 CPU 在事务到达 PCI 设备之前继续执行。硬件工程师称之为“写入后发”,因为写入完成在事务到达目的地之前被“后发”到 CPU。

因此,时间敏感的代码应添加 readl(),以便 CPU 在执行其他工作之前等待。经典的“位操作”序列在 I/O 端口空间中可以正常工作。

for (i = 8; --i; val >>= 1) {
        outb(val & 1, ioport_reg);      /* write bit */
        udelay(10);
}

MMIO 空间的相同序列应为

for (i = 8; --i; val >>= 1) {
        writeb(val & 1, mmio_reg);      /* write bit */
        readb(safe_mmio_reg);           /* flush posted write */
        udelay(10);
}

重要的是,“safe_mmio_reg” 不得有任何会干扰设备正确运行的副作用。

另一个需要注意的情况是重置 PCI 设备时。使用 PCI 配置空间读取来刷新 writel()。如果 PCI 设备预期不会响应 readl(),这将优雅地处理所有平台上的 PCI 主设备中止。大多数 x86 平台将允许 MMIO 读取导致主设备中止(也称为“软失败”),并返回垃圾数据(例如 ~0)。但是许多 RISC 平台会崩溃(也称为“硬失败”)。