英语

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

作者:

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

更完整的资源是 Jonathan Corbet、Alessandro Rubini 和 Greg Kroah-Hartman 编写的第三版“Linux Device Drivers”。LDD3 可从以下网址免费获得(在 Creative Commons 许可下):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 和流式 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 函数大部分定义为完全为空的内联函数,或者仅返回适当的错误代码,以避免驱动程序中出现大量 ifdefs。

1.2. pci_register_driver() 调用

PCI 设备驱动程序在其初始化期间调用 pci_register_driver(),其中包含指向描述驱动程序的结构的指针 (struct pci_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() 期间用于已存在的设备,或者在稍后插入新设备时),将调用此探测函数。对于 ID 表中条目与设备匹配的每个设备,此函数会传递一个 “struct pci_dev *”。当驱动程序选择获取设备的“所有权”时,探测函数返回零,否则返回错误代码(负数)。始终从进程上下文中调用探测函数,因此它可以休眠。

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 Local Bus Spec 的附录 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)。供应商和设备字段是必需的,其他字段是可选的。用户只需要传递尽可能多的可选字段

  • 子供应商和子设备字段默认为 PCI_ANY_ID (FFFFFFFF)

  • 类和 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 和流式 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/20060302180025.GC28895@flint.arm.linux.org.uk/

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. 初始化设备寄存器

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

1.4.6. 注册 IRQ 处理程序

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

应该使用 IRQF_SHARED 注册 IRQ 线的所有中断处理程序,并使用 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 可以分配几个单独的向量。

可以通过在调用 request_irq() 之前使用 PCI_IRQ_MSI 和/或 PCI_IRQ_MSIX 标志调用 pci_alloc_irq_vectors() 来启用 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 不会被处理并屏蔽 IRQ(100,000 次迭代后)。一旦屏蔽了共享 IRQ,剩余设备将停止正常工作。这不是一个好的情况。

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

1.5.2. 释放 IRQ

一旦设备静止(没有更多 IRQ),就可以调用 free_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/。pci.ids 文件的镜像位于 https://github.com/pciutils/pciids

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平台将会崩溃(也称为“硬故障”)。