PCI 不仅仅是一条总线。 它是一个标准,它有一套完整的规范,定义了计算机的不同部分应该如何交互。 多年来,PCI 总线已成为设备互连的事实总线标准,因此几乎每个 SoC 都具有对此类总线的本机支持。 对速度的需求导致了这种公交车的不同版本和不同的世代。
在该标准的早期,第一个实现 PCI 标准的总线是 PCI 总线(总线名称与标准相同),作为 ISA 总线的替代品。 这(利用 32 位寻址和无跳线自动检测和配置)改善了 ISA 遇到的地址限制(限制为 24 位,有时需要使用跳线才能路由 IRQ 等)。 与以前的 PCI 标准总线实现相比,提高的主要因素是速度。
PCI Express是当前的 PCI 总线系列。 它是串行总线,而它的祖先是并行的。 除了速度之外,PCIe 还将其前身的 32 位寻址扩展到 64 位,并在中断管理系统中进行了多项改进。 这个家族分为几代,GenX,我们将在本章的后续章节中看到这一点。 我们将从介绍 PCI 总线和接口开始,在这里我们将了解总线枚举,然后我们将查看 Linux 内核 PCIAPI 和核心功能。
所有这一切的好消息是,无论是什么家族,几乎所有的东西对驱动开发者来说都是透明的。 Linux 内核将抽象和隐藏一组减少的 API 后面的大部分机制,这些 API 可用于编写可靠的 PCI 设备驱动。
我们将在本章介绍以下主题:
- PCI 总线和接口简介
- Linux 内核 PCI 子系统及其数据结构
- PCI 和直接存储器访问(DMA)
需要对 Linux 内存管理和内存映射有一个很好的概述,还需要熟悉中断和锁定的概念,特别是对 Linux 内核的了解。
Linuxkernel v4.19.X 源代码可从https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags获得。
外围组件互连(PCI)是用于将外围硬件设备连接到计算机系统的本地总线标准。 作为总线标准,它定义了计算机的不同外围设备应该如何交互。 然而,多年来,PCI 标准无论是在功能方面还是在速度方面都发生了变化。 到目前为止,我们已经有几个总线系列实现了 PCI 标准,例如,如 PCI(是的,与标准同名的总线),以及PCI Extended(PCI-X)、PCI Express(PCIe或PCI-E),它是当前一代的 PCI。(PCI Extended(PCI-X)、PCI Express(PCIe或PCI-E)。 遵循 PCI 标准的总线称为 PCI 总线。
从软件的角度来看,所有这些技术都是兼容的,可以由相同的内核驱动处理。 这意味着内核不需要知道使用的确切总线变量。 从软件的角度来看,PCIe极大地扩展了PCI,有很多相似之处(尤其是读/写 I/O 或内存事务)。 虽然两者都是软件兼容的,但 PCIe 是串行总线而不是并行总线(在 PCIe 之前,每个 PCI 总线系列都是并行的),这也意味着您不能将 PCI 卡安装在 PCIe 插槽中,也不能将 PCIe 卡安装在 PCI 插槽中。
PCI Express 是当今计算机上最流行的总线标准,因此我们将在本章以 PCIe 为目标,并在必要时提到与 PCI 的相似或不同之处。 除此之外,以下是 PCIe 中的一些改进:
- PCIe 是串行总线技术,而 PCI(或其他实现)是并行的,因此减少了连接设备所需的 I/O 通道数量,从而降低了设计复杂性。
- PCIe 实现了增强的中断管理功能(提供基于消息的中断(又名 MSI 或其扩展版本 MSI-X),从而在不增加延迟的情况下扩展了 PCI 设备可以处理的中断数量。
- PCIe 提高了传输频率和吞吐量:Gen1、Gen2、Gen3...
PCI 设备是内存映射设备的类型。 连接到任何 PCI 总线的设备被分配处理器地址空间中的地址范围。 这些地址范围在 PCI 地址域中具有不同的含义,它包含三种不同类型的内存,具体取决于它们所包含的内容(基于 PCI 的设备的控制、数据和状态寄存器)或访问方式(I/O 端口或内存映射)。 设备驱动/内核将访问这些存储区域,以控制通过 PCI 总线连接的特定设备,并与其共享信息。
PCI 地址域包含三种不同的内存类型,它们必须映射到处理器的地址 space 中。
由于 PCIe 生态系统相当大,在深入讨论之前,我们可能需要熟悉一些术语。 这些资料如下:
- 根联合体(rc):指的是 SoC 中的 PCIe 主机控制器。 它可以在没有 CPU 干预的情况下访问主存储器,这是其他设备用来访问主存储器的特征。 它们也称为主机到 PCI 网桥。
- Endpoint(EP):端点是 PCIe 设备,由类型
00h
配置空间标头表示。 它们从不出现在交换机的内部总线上,也没有下游端口。 - 通道:这表示组差分信号对(一对用于 Tx,一对用于 Rx)。
- Link:这表示两个组件之间的双单工(实际上是一对)通信通道。 为了扩展带宽,链路可以聚合由
xN
(x1
、x2
、x4
、x8
、x12
、x16
和x32
)表示的多条通道,其中N
是对的数量。
并非所有 PCIe 设备都是端点。 它们也可以是交换机或网桥。
-
桥接器:它们提供到其他总线的接口,比如 PCI 或 PCIX,甚至另一条 PCIe 总线。 网桥还可以提供到同一总线的接口。 例如,PCI 到 PCI 桥通过创建一个完全独立的二级总线(我们将在接下来的章节中看到二级总线是什么),方便了向总线添加更多的负载。 网桥的概念有助于理解和实施交换机的概念。
-
Switches: These provide an aggregation capability and allow more devices to be attached to a single root port. It goes without saying that switches have a single upstream port, but may have several downstream ports. They are smart enough to act as packet routers and recognize which path a given packet will need to take based on its address or other routing information, such as an ID. That being said, there is also implicit routing, which is used only for certain message transactions, such as broadcasts from the root complex and messages that always go to the root complex.
交换机下游端口是从内部总线桥接到表示此 PCI Express 交换机的下游 PCI Express 链路的总线的(虚拟)PCI-PCI 桥。 请记住,只有代表交换机下游端口的 PCI-PCI 网桥才可能出现在内部总线上。
重要音符
PCI 到 PCI 桥提供两条外围组件互连(PCI)总线之间的连接路径。 您应该记住,在总线枚举期间只考虑 PCI-PCI 桥的下游端口。 这对于在下支持枚举进程是非常重要的。
与 PCI 相比,PCIe 最明显的改进是它的点对点总线拓扑。 每个设备都位于自己的专用总线上,在 PCIe 行话中,它被称为链路。 了解 PCIe 设备的枚举过程需要一些知识。
当您查看设备的寄存器空间(在标题类型寄存器中)时,它们会说它们是类型0
还是类型1
寄存器空间。 通常,类型0
表示端点设备,类型1
表示网桥设备。 该软件必须识别它是与端点设备通话还是与网桥设备通话。 网桥设备配置与端点设备配置不同。 在网桥设备(类型 1)枚举期间,软件必须为其分配以下元素:
- 主总线号:这是上游总线号。
- 次级/从属总线号:这给出了特定 PCI 网桥的下游总线号的范围。 次要总线号是紧接在 PCI-PCI 桥下游的总线号,而从属总线号表示桥下游可到达的所有总线中的最高总线号。 在枚举的第一阶段,由于
255
是最高的总线编号,所以从属总线编号字段被赋予值0xFF
。 当枚举继续时,此字段将被赋予此桥可以向下游延伸到多远的实际值。
设备标识由使设备唯一或可寻址的几个属性或参数组成。 在 PCI 子系统中,这些参数如下:
- 供应商 ID:此标识设备的制造商。
- 设备 ID:此标识特定供应商设备。
前面两个元素可能就足够了,但您也可以依赖以下元素:
- 修订版 ID:此指定特定于设备的修订版标识符。
- 类代码:此标识设备实现的通用功能。
- 页眉类型:此定义页眉的布局。
所有这些参数都可以从设备配置寄存器 REGI 中读取。 这就是内核在枚举总线时识别设备 wh 所做的事情。
在深入研究 PCIe 总线枚举函数之前,我们需要注意一些基本限制:
- 系统(
0-255
)上可以有256
条总线,因为有8
位来标识它们。 - 每条总线(
0-31
)可以有32
个设备,因为每条总线上都有5
位来标识它们。 - 一个设备最多可以有 8 个功能(
0-7
),因此可以使用3
位来标识它们。
所有外部 PCIe 通道,无论它们是否源自 CPU,都位于 PCIe 网桥之后(因此获得新的 PCIe 总线编号)。 组态软件最多可以枚举给定系统上的256
条 PCI 总线。 数字0
总是分配给根复合体。 请记住,在总线枚举过程中只考虑 PCI-PCI 网桥的下游端口(次要侧)。
PCI 枚举过程基于深度优先搜索(DFS)算法,该算法通常从随机节点开始(但在 PCI 枚举的情况下,该节点是预先知道的,在我们的例子中是 RC),并且在回溯之前尽可能地沿着每个分支探索(实际寻找桥)。
这样说来,当找到一座桥时,组态软件会给它分配一个编号,至少比这座桥所在的总线编号大一个。 在此之后,配置软件开始在此新总线上查找新网桥,依此类推,然后返回到此网桥的同级网桥(如果网桥是多端口交换机的一部分)或邻居网桥(就拓扑而言)。
枚举的设备使用 BDF 格式标识,该格式代表bus-device-function,它使用十六进制(不带0x
)表示法的三个字节(即XX:YY:ZZ
)进行标识。 例如,00:01:03
的字面意思是公交车0x00: Device 0x01: Function 0x03
。 我们可以将其解释为总线0
上的器件1
的函数3
。 此符号有助于快速定位给定拓扑中的设备。 如果使用双字节表示法,这将意味着该函数已被省略或无关紧要,换句话说,XX:YY
。
下图显示了 PCIe 交换矩阵的拓扑:
图 11.1-PCI 总线枚举
在我们描述前面的拓扑图之前,请重复以下四条语句,直到您熟悉它们:
- PCI 到 PCI 桥通过创建完全独立的二级总线来方便向总线添加更多负载。 因此,每个网桥下游端口都是一个新的总线,必须为其分配一个总线号,该总线号至少比它所在的总线号大+1。
- 交换机下游端口是从内部总线桥接到表示该 PCI Express 交换机的下行 PCI Express 链路的总线的(种类的虚拟)PCI-PCI(P2P)桥。
- CPU 通过主机到 PCI 网桥(代表根联合体中的上游网桥)连接到根联合体。
- 在总线枚举期间只考虑 PCI-PCI 桥的下游端口。
在将枚举算法应用于图中的拓扑之后,我们可以列出从A到J的 10 个步骤。 步骤A和B位于根联合体内部,根联合体承载总线0
以及两个网桥(从而提供两条总线)00:00:00
和00:01:00
。 尽管步骤C是标准化枚举逻辑的起点,但下面描述了前面拓扑图中枚举过程中的步骤:
00:00
作为一个(虚拟)网桥,毫无疑问,它的下游端口是一条总线。 然后为其分配编号1
(请记住,它始终大于网桥所在的总线编号,在本例中为0
)。 然后枚举总线1
。- 步骤C:在总线
1
上有一个开关(一个提供其内部总线的上游虚拟网桥和两个暴露其输出总线的下游虚拟网桥)。 此交换机的内部总线编号为2
。 - 我们立即进入步骤D,在此找到第一个下游网桥,并在为其分配总线号
3
后枚举其总线,其后面有一个端点(没有下游端口)。 根据 DFS 算法的原理,我们到达了这个分支的叶子节点,这样就可以开始回溯了。 - 因此,在步骤E中,作为在步骤D中找到的虚拟网桥的兄弟的虚拟网桥已经找到它的位置。 然后为其总线分配总线号
4
,并且在其后面有一个设备。 回溯可能会再次发生。 - 然后,我们到达步骤F,其中为在步骤 B 中找到的虚拟网桥分配一个总线号,即
5
。 在该总线后面有一个交换机(一个实现内部总线的上游虚拟网桥,其总线号为6
,以及代表其外部总线的3
下游虚拟网桥,因此这是一个 3 端口交换机)。 - 步骤G是找到 3 端口交换机的第一个下游虚拟网桥的位置。 它的总线被赋予总线编号
7
,并且该总线后面有一个端点。 如果我们必须使用 BDF 格式标识该端点的函数0
,那么它将是07:00:00
(总线7
上的设备0
的函数0
)。 回到 DFS 算法,我们已经到达了分支的底部。 然后我们可以开始回溯,这将我们引向步骤H。 - 在步骤H中,找到 3 端口交换机中的第二下游虚拟网桥。 它的总线被分配了总线编号
8
。 此总线后面有一个 PCIe 到 PCI 桥。 - 在步骤i中,该 PCIe 到 PCI 桥被分配有下游总线号
9
,并且在该总线后面有一个 3 功能端点。 在 BDF 表示法中,它们将被标识为09:00:00
、09:00:01
和09:00:02
。 因为端点标记分支的深度,所以它允许我们执行另一次回溯,这将我们带到步骤J。 - 在回溯阶段,我们进入步骤J。 找到 3 端口交换机的第三个也是最后一个下游虚拟网桥,并为其总线指定总线号
10
。 此总线后面有一个端点,该端点在 BDF 格式中将被标识为0a:00:00
。 这标志着枚举过程的结束。
乍一看,PCI(E)总线枚举可能看起来很复杂,但它相当简单。 把前面的材料读两遍就足以理解整个过程。
根据地址空间的内容或访问方法,PCI 目标最多可以实现三种不同类型的地址空间。 它们是配置地址空间、存储器地址空间和I/O 地址空间。 配置和内存地址空间是内存映射的-它们从系统地址空间分配了个地址范围,因此对该地址范围的读取和写入不会进入 RAM,而是直接从 CPU 路由到设备,而 I/O 地址空间则不是。 不再赘述,让我们分析它们之间的不同之处以及它们的不同用例。
这是地址空间,从中可以访问设备的配置,并存储有关设备的基本信息,操作系统还使用该地址空间根据设备的操作设置对设备进行编程。 PCI 上的配置空间有256
个字节。 PCIe 将其扩展到4
KB 的寄存器空间。 因为配置地址空间是内存映射的,所以指向配置空间的任何地址都是从系统内存映射中分配的。 因此,这些4
KB 空间从系统存储器映射中分配存储器地址,但实际的值/位/内容通常在外围设备的寄存器中实现。 例如,当您读取供应商 ID 或设备 ID 时,即使使用的内存地址来自系统内存映射,目标外围设备也会返回数据。
此地址空间的一部分是标准化的。 配置地址空间按如下方式拆分:
- 前
64
字节(00h
-3Fh
)表示用于标识设备的标准配置头,包括 PCI 总线 ID、供应商 ID 和设备 ID 寄存器。 - 剩余的
192
字节(40h
-FFh
)构成用户定义的配置空间,例如 PC 卡附带的软件驱动使用的特定信息。
一般来说,配置空间存储有关设备的基本信息。 它允许中央资源或操作系统使用操作设置对设备进行编程。 没有与配置地址空间关联的物理内存。 它是在TLP(事务层分组)中使用的地址列表,用于标识事务的目标。
用于在 PCI 设备的每个配置地址空间之间传输数据的命令称为配置环 ad命令或配置写入命令。
如今,I/O 地址空间用于与 x86 架构的 I/O 端口地址空间兼容。 PCIe 规范不鼓励使用此地址空间。 如果 PCI Express 规范的未来修订版不建议使用 I/O 地址空间,也就不足为奇了。 I/O 映射 I/O 的唯一优点是,由于其独立的地址空间,它不会从系统内存空间窃取地址范围。 因此,计算机可以在 32 位系统上访问整个个 4 GB 的 RAM。
I/O READ和I/O WRITE命令用于在I/O 地址空间中传输数据。
在计算机的早期,英特尔定义了一种通过所谓的 I/O 地址空间访问 I/O 设备中的寄存器的方法。 这在当时是有意义的,因为处理器的内存地址空间相当有限(例如,想想 16 位系统),使用它的某些范围来访问设备几乎没有意义。 当系统内存空间的限制变得不那么大时(例如,考虑 32 位系统,其中 CPU 最多可以寻址 4 GB),I/O 地址空间和内存地址空间之间的分离就变得不那么重要,甚至是繁重的负担。
该地址空间有如此多的限制和约束,导致 I/O 设备中的寄存器直接映射到系统的内存地址空间,因此称为内存映射 I/O(MMIO)。 这些限制和约束包括以下内容:
- 需要一辆专用公交车
- 单独的指令集
- 由于它是在 16 位系统时代实现的,端口地址空间仅限于
65536
个端口(相当于 216 个),尽管非常老的机器使用 10 位作为 I/O 地址空间,并且只有 1024 个唯一的端口地址
因此,利用内存映射 I/O 的优势变得更加实际。
内存映射 I/O 允许通过使用普通内存访问指令简单地读取或写入那些“特殊”地址来访问硬件设备,尽管与 65536 相比,解码高达 4 GB(或更多)地址的成本更高。 也就是说,PCI 设备通过名为 BARs 的窗口公开它们的内存区域。 一个 PCI 设备最多可以有六个条。
bar代表基址寄存器,是一个 PCI 概念,设备通过它告诉主机它需要多少内存,以及它的类型。 这是内存空间(从系统内存映射中获取),而不是实际的物理 RAM(您实际上可以将 RAM 本身视为一个“专门的内存映射 I/O 设备”,其工作只是保存和回馈数据,尽管对于今天的带有缓存等功能的现代 CPU,这在物理上并不简单)。 BIOS 或操作系统负责将请求的内存空间分配给目标设备。
一旦被分配,条就被主机系统(CPU)视为与设备对话的内存窗口。 设备本身不会写入该窗口。 这个概念可以看作是访问 PCI 设备内部和本地的实际物理内存的间接机制。
实际上,存储器的实际物理地址和输入/输出寄存器的地址都在 PCI 设备内部。 下面是主机如何处理外围设备的存储空间:
- 外围设备通过某种方式告诉系统它有几个存储间隔和 I/O 地址空间,每个间隔有多大,以及它们各自的本地地址。 显然,这些地址都是本地地址和内部地址,都是从
0
开始的。 - 在系统软件知道有多少个外围设备以及它们具有什么样的存储间隔后,它们可以为这些间隔分配“物理地址”,并在这些间隔和总线之间建立连接。 这些地址是可访问的。 显然,这里的所谓“物理地址”与真实的物理地址有些不同。 它实际上是一个逻辑地址,因此它通常成为“总线地址”,因为这是 CPU 在总线上看到的地址。 正如您可以想象的那样,外围设备 l 上一定有某种类型的地址映射机制。所谓的“外围设备地址分配”是为它们分配 b 用户地址并建立映射。
这里我们将讨论 PCI 设备处理中断的方式。 PCI Express 中有三种中断类型。 这些资料如下:
- 传统中断,也称为 INTX 中断,是旧 PCI 实现中唯一可用的机制。
- MSI(基于消息的中断)扩展了传统机制,例如,通过增加可能的中断数量。
- MSI-X(扩展 MSI)扩展并增强了 MSI,例如,允许将单个中断定向到不同的处理器(在某些高速网络应用中很有用)。
PCI Express 端点中的应用逻辑可以实现上述三个方法中的一个或多个,以 s 发信号通知中断。 让我们来详细看看这些。
传统中断管理基于 PCI INT-X 中断线,最多由四条虚拟中断线组成,称为 INTA、INTB、INTC 和 INTD。 这些中断线由系统中的所有 PCI 设备共享。 以下是传统实现为了识别和处理中断而必须经历的步骤:
- 该器件断言其 INT#引脚之一以产生中断。
- CPU 确认中断,并通过调用它们的中断处理程序轮询连接到该 int#行(共享)的每个设备(实际上是它的驱动)。 服务中断所需的时间取决于共享线路的设备的数量。设备的中断服务例程(ISR)可以通过读取设备的内部寄存器来确定中断的原因,从而检查中断是否源自该设备。
- ISR 采取行动来服务中断。
在上述方法和传统方法中,中断线路是共享的:每个人都接听电话。 此外,物理中断线是有限的。 在下一节中,我们将了解 MSI 如何解决这些问题并促进中断管理。
重要音符
I.MX6 分别将 INTA/B/C/D 映射到 ARM GIC IRQ155
/154
/153
/152
。 这使得 PCIe 到 PCI 网桥能够正常工作。 请参阅 IMX6DQRM.pdf,第 225 页。
有两种基于消息的中断机制:MSI 和 MSI-X,增强和扩展版本。 MSI(或 MSI-X)只是使用 PCI Express 协议层发出中断信号的一种方式,而 PCIe 根联合体(主机)负责中断 CPU。
传统上,设备被分配引脚作为中断线路,当它想要向 CPU 发出中断信号时,必须断言该中断线路。 这种信令方法是带外的,因为它使用另一种方式(不同于主数据路径)来发送这种控制信息。
然而,MSI 允许设备将少量描述中断的数据写入特殊的内存映射 I/O 地址,然后根联合体负责将相应的中断传递给 CPU。 一旦端点设备想要产生 MSI 中断,它就用消息数据寄存器中指定的数据内容向(目标)消息地址寄存器中指定的地址发出写请求。 由于数据路径用于此,因此它是带内机制。 此外,MSI 增加了可能的中断数量。 这将在下一节中介绍。
重要音符
PCI Express 完全没有单独的中断引脚。 但是,它在软件级别上与传统中断兼容。 为此,它需要 MSI 或 MSI-X,因为它使用特殊的带内消息来允许模拟引脚断言或取消断言。 换句话说,PCI Express 通过提供assert_INTx
和deassert_INTx
来模拟此功能。 消息包通过 PCI Express 串行链路发送。
在使用 MSI 的实现中,通常有以下步骤:
- 该器件通过向上游发送 MSI 存储器写入来产生中断。
- CPU 确认中断并调用适当的器件 ISR,因为这是基于 MSI 矢量预先知道的。
- ISR 采取行动来服务中断。
MSI 不是共享的,因此分配给设备的 MSI 在系统中保证是唯一的。 不用说,MSI 实现大大减少了中断所需的总服务时间。
重要音符
大多数人认为 MSI 允许设备将数据作为中断的一部分发送到处理器。 这是一种误解。 事实是,作为内存写入事务的一部分发送的数据仅由芯片组(实际上是根联合体)用来确定在哪个处理器上触发哪个中断;该数据不可用于设备向中断处理程序通信附加信息。
MSI 最初定义为 PCI 2.2 标准的一部分,允许设备分配 1、2、4、8、16 或最多 32 个中断。 该器件被编程以写入地址以发出中断信号(通常是中断控制器中的控制寄存器),以及用于标识器件的 16 位数据字。 将中断号添加到数据字以标识中断。
PCI Express 端点可以通过向根端口发送标准 PCI Express POST 写入分组来发信号通知 MSI。 数据包由特定地址(由主机分配)和主机提供给端点的多达 32 个数据值(因此,32 个中断)之一组成。 与传统中断相比,变化的数据值和地址值提供了更详细的中断事件标识。 在 MSI 规范中,中断屏蔽功能是可选的。
这种方法确实有一些限制。 32 个数据值只使用一个地址,这使得很难将单个中断定向到不同的处理器。 这种限制是因为与 MSI 相关联的存储器写操作只能通过它们的目标地址位置(而不是数据)来与其他存储器写操作区分开来,这些地址位置是由系统为中断传送而保留的。
以下是 PCI Express 设备的 PCI 控制器驱动执行的 MSI 配置步骤:
- 总线枚举过程在启动期间进行。 它包括内核 PCI 核心代码扫描 PCI 总线以发现设备(换句话说,它执行有效供应商 ID 的配置读取)。 当发现 PCI Express 函数时,PCI 核心代码读取能力列表指针以获得寄存器链内的第一能力寄存器的位置。
- 然后,PCI 核心代码搜索能力寄存器组。 它一直这样做,直到它发现 MSI 能力寄存器集(能力 ID 为
05h
)。 - 之后,PCI 核心代码配置设备,将内存地址分配给设备的消息地址寄存器。 这是发送中断请求时使用的内存写入的目标地址。
- PCI 核心代码检查设备的消息控制寄存器中的多消息能力字段,以确定设备希望向其分配多少特定于事件的消息。
- 然后,核心代码分配等于或小于设备请求的消息数量。 作为最低要求,将向该设备分配一条消息。
- 核心代码将基本消息数据模式写入设备的消息数据寄存器。
- 最后,PCI 核心代码在设备的消息控制寄存器中设置 MSI 使能位,从而使其启用,以使用 MSI 存储器写入生成中断。
MSI-X只是 PCIe 中 PCI MSI 的扩展-它具有相同的功能,但可以承载更多信息,并且更灵活。 请注意,PCIe 同时支持 MSI 和 MSI-X。 MSI-X 最初是在 PCI 3.0(PCIe)标准中定义的。 它允许设备支持至少 64 个(最小 MSI 中断,但是最大 MSI 中断的两倍)到最多 2,048 个中断。 实际上,MSI-X 允许更多的中断,并为每个中断提供单独的目标地址和数据字。 由于发现原始 MSI 使用的单个地址对某些架构有限制,因此启用 MSI-X 的设备使用地址和数据对,从而允许设备使用多达2048
个地址和数据对。 多亏了每个端点可用的大量地址值,才有可能将 MSI-X 消息路由到系统中的不同中断使用者,这与 MSI 数据包可用的单个地址不同。 此外,具有 MSI-X 功能的端点还包括屏蔽和保存挂起中断的应用逻辑,以及用于地址和数据对的内存表。
除此之外,MSI-X 中断与 MSI 相同。 但是,MSI-X 强制要求 MSI 中的可选功能(如 64 位地址和中断屏蔽)。
因为 PCIe 声称向后兼容传统并行 PCI,所以它还需要支持基于 INTX 的中断机制。 但是,如何才能做到这一点呢? 实际上,在传统 PCI 系统中有四条 INTx(INTA、INTB、INTC 和 INTD)物理 IRQ 线路,它们都是电平触发的,实际上是低有效的(换句话说,只要物理 INTX 线路处于低电压,中断请求就是有效的)。 那么每个 IRQ 在仿真版本中是如何传输的呢?
答案是 PCIe 通过使用带内信令机制(即所谓的 MSI)来虚拟化 PCI 物理中断信号。 由于每条物理线路有两个级别(断言和取消断言),因此 PCIe 每条线路提供两条消息,称为assert_INTx
和deassert_INTx
消息。 总共有八种消息类型:assert_INTA
、deassert_INTA
、...assert_INTD
、deassert_INTD
。 事实上,它们被简称为 INTX 消息。 这样,INTX 中断就像 MSI 和 MSI-X 一样在 PCIe 链路上传播。
这种向后兼容性主要用于 PCI 到 PCIe 桥芯片的 STS,以便 PCI 设备可以在不修改驱动的情况下在 PCIe 系统中正常工作。
现在我们熟悉了 PCI 子系统中的中断分配。 我们讨论了传统的基于 int-X 的机制和基于消息的机制。 现在是深入研究代码的时候了,从数据结构到 API。
Linux 内核支持 PCI 标准,并提供 API 来处理这样的设备。 在 Linux 中,PCI 实现大致可以分为以下主要组件:
-
PCI BIOS: This is an architecture-dependent part in charge of kicking off the PCI bus initialization. ARM-specific Linux implementation lies in
arch/arm/kernel/bios32.c
. The PCI BIOS code interfaces with PCI Host Controller code as well as the PCI core in order to perform bus enumeration and the allocation of resources, such as memory and interrupts.BIOS 执行的成功完成保证了系统中的所有 PCI 设备都被分配了部分可用的 PCI 资源,并且它们各自的驱动(称为从驱动或端点驱动)可以使用 PCI 核心提供的设施来控制它们。
在这里,内核调用体系结构和特定于板卡的 PCI 功能的服务。 这里完成了 PCI 配置的两个重要任务。 第一个任务是扫描总线上的所有 PCI 设备,配置它们,并分配内存资源。 第二项任务是配置设备。 此处配置意味着保留了资源(内存)并分配了 IRQ。 这并不意味着已初始化。 初始化是特定于设备的,应该由设备驱动完成。 PCI BIOS 可以选择跳过资源分配(如果它们是在 Linux 启动之前分配的,例如,在 PC 方案中)。
-
主机控制器(根联合体):此部件特定于 SoC(位于
drivers/pci/host/
,换句话说,对于 r-car SoC,位于drivers/pci/controller/pcie-rcar.c
)。 但是,某些 SoC 可能会实施来自给定供应商的相同 PCIe IP 块,例如 Synopsys DesignWare。 这样的控制器可以在相同的目录中找到,比如内核源代码中的drivers/pci/controller/dwc/
。 例如,其 PCIe IP 块来自该供应商的 i.MX6 具有用drivers/pci/controller/dwc/pci-imx6.c
实现的驱动。该部件处理特定于 SoC(有时是主板)的初始化和配置,并可能调用 PCI BIOS。 但是,它应该为 BIOS 和 PCI 核心提供 PCI 总线访问和设施回调函数,这些函数将在 PCI 系统初始化期间和在配置周期访问 PCI 总线时被调用。 此外,它还提供可用内存/IO 空间、INTx 中断线和 MSI 的资源信息。 它应便于 IO 空间访问(如受支持),并且可能还需要提供间接内存访问(如果硬件支持)。 -
Core(
drivers/pci/probe.c
):负责为系统中的总线、设备和网桥创建和初始化数据结构树。 它处理总线/设备编号。 它创建设备条目并提供proc/sysfs
信息。 它还为 PCI BIOS 和从设备(End Point)驱动提供服务,并提供可选的热插拔支持(如果硬件支持)。 它以(EP)驱动接口查询为目标,并初始化枚举期间找到的相应设备。 它还提供 MSI 中断处理框架和 PCI Express 端口总线支持。 所有这些都足以促进 Linux 内核中设备驱动的开发。
Linux kernelPCI 框架帮助开发 PCI 设备驱动,这些驱动构建在两个主要数据结构之上:struct pci_dev
,表示内核中的 PCI 设备 from;和struct pci_driver
,表示 PCI 驱动。
这是内核用来实例化系统上的每个 PCI 设备的结构。 它描述设备并存储其一些状态参数。 该结构在include/linux/pci.h
中定义如下:
struct pci_dev {
struct pci_bus *bus; /* Bus this device is on */
struct pci_bus *subordinate; /* Bus this device bridges to */
struct proc_dir_entry *procent;
struct pci_slot *slot;
unsigned short vendor;
unsigned short device;
unsigned short subsystem_vendor;
unsigned short subsystem_device;
unsigned int class;
/* 3 bytes: (base,sub,prog-if) */
u8 revision; /* PCI revision, low byte of class word */
u8 hdr_type; /* PCI header type (multi' flag masked out) */
u8 pin; /* Interrupt pin this device uses */
struct pci_driver *driver; /* Driver bound to this device */
u64 dma_mask;
struct device_dma_parameters dma_parms;
struct device dev;
int cfg_size;
unsigned int irq;
[...]
unsigned int no_msi:1; /* May not use MSI */
unsigned int no_64bit_msi:1; /* May only use 32-bit MSIs */
unsigned int msi_enabled:1;
unsigned int msix_enabled:1; atomic_t enable_cnt;
[...]
};
在前面的块中,出于可读性的考虑,删除了一些元素。 对于剩余的,以下元素具有以下含义:
-
procent
是/proc/bus/pci/
中的设备条目。 -
slot
是此设备所在的物理插槽。 -
vendor
是设备制造商的供应商 ID。 PCI 特殊利益集团维护着这类号码的全球注册,制造商必须申请为其分配一个唯一的号码。 该 ID 存储在设备配置空间的 16 位寄存器中。 -
device
是探测到此特定设备后标识该设备的 ID。 这取决于供应商,因此没有官方注册。 这也存储在 16 位寄存器中。 -
subsystem_vendor
和subsystem_device
指定 PCI 子系统供应商和子系统设备 ID。 正如我们在前面看到的,它们可以用来进一步识别设备。 -
class
标识此设备所属的类别。 它存储在 16 位寄存器中(在设备配置空间中),其最高 8 位标识基类或基组。 -
pin
是该器件使用的中断引脚,对于传统的基于 INTX 的中断。 -
driver
是与此设备关联的驱动。 -
dev
是此 PCI 设备的底层设备结构。 -
cfg_size
是配置空间的大小。 -
irq
is the field that is worth spending time on. When the device boots, MSI(-X) mode is not enabled and it remains unchanged until it is explicitly enabled by means of thepci_alloc_irq_vectors()
API (old drivers usepci_enable_msi()
).因此,
irq
首先对应于默认的预分配非 MSI IRQ。 但是,其值或用法可能会根据以下情况之一发生变化:A)在 MSI 中断模式下(在设置了
PCI_IRQ_MSI
标志的情况下成功调用pci_alloc_irq_vectors()
时),该字段的(预先分配的)值被新的 MSI 矢量替换。 此向量对应于已分配向量的基本中断号,因此与向量 X(从 0 开始的索引)对应的 IRQ 号等同于(与)pci_dev->irq + X
(参见pci_irq_vector()
函数,该函数旨在返回设备向量的 Linux IRQ 号)。B)在 MSI-X 中断模式下(在设置了
PCI_IRQ_MSIX
标志的情况下成功调用pci_alloc_irq_vectors()
时),该字段的(预先分配的)值不受影响(因为每个 MSI-X 矢量都有其专用的报文地址和报文数据对,这不需要 1:1 矢量到条目的映射)。 但是,在此模式下,irq
无效。 在驱动中使用它来请求服务中断可能会导致不可预知的行为。 因此,如果需要 MSI(-X),则应在驱动调用devm_equest_irq()
之前调用pci_alloc_irq_vectors()
函数(该函数使 MXI(-X)先于分配向量),因为 MSI(-X)是通过与基于管脚的中断的向量不同的向量传递的。 -
msi_enabled
保持 MSI IRQ 模式的启用状态。 -
msix_enabled
保持 MSI-X IRQ 模式的启用状态。 -
enable_cnt
保存调用pci_enable_device()
的次数。 这有助于在pci_enable_device()
的所有调用方都调用pci_disable_device()
之后,真正禁用设备。
struct pci_dev
描述设备,而struct pci_device_id
用于标识设备。 该结构定义如下:
struct pci_device_id {
u32 vendor, device;
u32 subvendor, subdevice;
u32 class, class_mask;
kernel_ulong_t driver_data;
};
为了理解该结构对 PCI 驱动的重要性,我们来描述一下它的每个元素:
vendor
和device
分别表示设备的供应商 ID 和设备 ID。 两者成对使用,构成设备的唯一 32 位标识符。 驱动依赖此 32 位标识符来标识其设备。subvendor
和subdevice
表示子系统 ID。class
、class_mask
是与类相关的 PCI 驱动,旨在处理给定类的每个设备。 对于此类驱动器,应将vendor
和device
设置为PCI_ANY_ID
。 PCI 规范中描述了不同类别的 PCI 设备。 这两个值允许驱动指定它支持一种 PCI 类设备。driver_data
是驱动私有的数据。 此字段不用于标识设备,而是传递不同的数据以区分不同的设备。
有三个宏可用于创建struct pci_device_id
的特定实例:
PCI_DEVICE
:此宏用于通过创建struct pci_device_id
来描述特定的 PCI 设备,该struct pci_device_id
将特定的 PCI 设备与作为参数(PCI_DEVICE(vend,dev)
)给定的供应商和设备 ID 相匹配,并且子供应商、子设备和与类相关的字段设置为PCI_ANY_ID
。PCI_DEVICE_CLASS
:此宏用于通过创建将特定 PCI 类与作为参数(PCI_DEVICE_CLASS(dev_class,dev_class_mask)
)的class
和class_mask
相匹配的struct pci_device_id
来描述特定的 PCI 设备类。 供应商、设备、子供应商和子设备字段将设置为PCI_ANY_ID
。 一个典型的例子是PCI_DEVICE_CLASS(PCI_CLASS_STORAGE_EXPRESS, 0xffffff)
,它对应于 NVMe 设备的 PCI 类,并且无论供应商和设备 ID 是什么,它都将匹配其中的任何一个。PCI_DEVICE_SUB
:此宏用于通过创建将特定设备与作为参数(PCI_DEVICE_SUB(vend, dev, subvend, subdev)
)给出的子系统信息相匹配的struct pci_device_id
来描述具有子系统的特定 PCI 设备。
驱动支持的每个设备/类都应该输入到相同的数组中以备后用(我们将在两个地方使用它),如下例所示:
static const struct pci_device_id bt8xxgpio_pci_tbl[] = {
{ PCI_DEVICE(PCI_VENDOR_ID_BROOKTREE, PCI_DEVICE_ID_BT848) },
{ PCI_DEVICE(PCI_VENDOR_ID_BROOKTREE, PCI_DEVICE_ID_BT849) },
{ PCI_DEVICE(PCI_VENDOR_ID_BROOKTREE, PCI_DEVICE_ID_BT878) },
{ PCI_DEVICE(PCI_VENDOR_ID_BROOKTREE, PCI_DEVICE_ID_BT879) },
{ 0, },
};
每个pci_device_id
结构都需要导出到用户空间,以便让热插拔和设备管理器(udev
、mdev
等)。 了解哪个驱动与哪个设备配套。 将它们全部送入同一阵列的第一个原因是,它们可以在一次拍摄中导出。 要实现这一点,应使用MODULE_DEVICE_TABLE
宏,如下例所示:
MODULE_DEVICE_TABLE(pci, bt8xxgpio_pci_tbl);
此宏使用给定信息创建自定义节。 在编译时,构建过程(更准确地说是depmod
)从驱动中提取此信息,并构建一个名为modules.alias
的人类可读表,该表位于/lib/modules/<kernel_version>/
目录中。 当内核告诉热插拔系统有新设备可用时,hotplug 系统将参考modules.alias
文件来查找要加载的正确驱动。
此结构表示 PCI 设备驱动的一个实例,无论它是什么,也不管它属于什么子系统。 它是每个 PCI 驱动必须创建和填充的主要结构,以便能够将它们注册到内核。 struct pci_driver
定义如下:
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); /* Device woken up */
void (*shutdown) (struct pci_dev *dev); [...]
};
这个结构中的部分元素已经被移除,因为它们对我们没有兴趣。 下面是结构中其余字段的含义:
-
name
:这是驱动的名称。 因为驱动是通过它们的名称来标识的,所以它在内核中的所有 PCI 驱动中必须是唯一的。 通常将此字段设置为与驱动的模块名称相同的名称。 如果同一子系统总线中已有同名的驱动寄存器,则您的驱动注册将失败。 要了解它在幕后是如何工作的,请查看https://elixir.bootlin.com/linux/v4.19/source/drivers/base/driver.c#L146上的driver_register()
。 -
id_table
:这应该指向前面描述的struct pci_device_id
表。 这是该结构在驱动中使用的第二个也是最后一个位置。 它必须为非空,才能调用探测。 -
probe
:这是指向驱动的probe
函数的指针。 当 PCI 设备与驱动的id_table
中的条目匹配(通过供应商/产品 ID 或类别 ID)时,PCI 核心将调用它。 如果此方法成功初始化设备,则应返回0
,否则返回负错误。 -
remove
:当此驱动处理的设备从系统中移除(从总线上消失)或从内核卸载驱动时,PCI 核心会调用此函数。 -
suspend
,resume
, andshutdown
: These are optional but recommended power management functions. In those callbacks, you can use PCI-related power management helpers such aspci_save_state()
orpci_restore_state()
,pci_disable_device()
orpci_enable_device()
,pci_set_power_state()
, andpci_choose_state()
. These callbacks are invoked by the PCI core, respectively:-设备挂起时,状态作为回调的参数给出。
-当设备正在恢复时。 这可能仅在调用
suspend
之后发生。-用于设备的正常关闭。
以下是正在初始化的 PCI 驱动结构的示例:
static struct pci_driver bt8xxgpio_pci_driver = {
.name = "bt8xxgpio",
.id_table = bt8xxgpio_pci_tbl,
.probe = bt8xxgpio_probe,
.remove = bt8xxgpio_remove,
.suspend = bt8xxgpio_suspend,
.resume = bt8xxgpio_resume,
};
向 PCI 核心注册 PCI 驱动包括调用pci_register_driver()
,给出一个参数作为指向前面设置的struct pci_driver
结构的指针。 这应该在模块的init
方法中完成,如下所示:
static int init pci_foo_init(void)
{
return pci_register_driver(&bt8xxgpio_pci_driver);
}
pci_register_driver()
如果注册时一切正常,则返回0
,否则返回负错误。 此返回值由内核处理。
但是,在模块的卸载路径上,需要取消注册struct pci_driver
,这样系统就不会尝试使用相应模块已不存在的驱动。 因此,卸载 PCI 驱动需要调用pci_unregister_driver()
,以及指向与注册相同的结构的指针,如下所示。 这应在模块exit
函数中完成:
static void exit pci_foo_exit(void)
{
pci_unregister_driver(&bt8xxgpio_pci_driver);
}
也就是说,由于这些操作经常在 PCI 驱动中重复,因此 PCI 核心公开module_pci_macro()
宏,以便自动处理注册/注销,如下所示:
module_pci_driver(bt8xxgpio_pci_driver);
这个宏更安全,因为它同时负责注册和注销,防止一些开发人员提供一个而忘记另一个。
现在我们熟悉了最重要的 PCI 数据结构-struct pci_dev
、pci_device_id
和pci_driver
,以及用于处理这些数据结构的连字符助手。 逻辑上的延续是驱动结构,在该结构中,我们了解在哪里以及如何使用前面列举的数据结构。
在编写 PCI 设备驱动时,需要遵循一些步骤,其中一些步骤需要按照预定义的顺序执行。 她的 e,我们尝试详细讨论每个步骤,并在适用的情况下解释细节。
在 PCI 设备上执行任何操作之前(甚至仅用于读取其配置寄存器),必须启用此 PCI 设备,这必须由代码显式完成。 内核为此提供了pci_enable_device()
。 此函数初始化设备,以便驱动可以使用它,要求低级代码启用 I/O 和内存。 它还处理 PCI 电源管理唤醒,这样如果设备挂起,它也会被唤醒。 下面是pci_enable_device()
的外观:
int pci_enable_device(struct pci_dev *dev)
由于pci_enable_device()
可能失败,因此必须检查它返回的值,如下例所示:
int err;
err = pci_enable_device(pci_dev); if (err) {
printk(KERN_ERR "foo_dev: Can't enable device.\n");
return err;
}
请记住,pci_enable_device()
将同时初始化内存映射条和 I/O 条。 但是,您可能想要初始化一个而不是另一个,因为您的设备不同时支持这两个,或者因为您不会在驱动中同时使用这两个。
为了不初始化 I/O 空间,您可以使用启用方法的另一个变体pci_enable_device_mem()
。 另一方面,如果您只需要处理 I/O 空间,则可以使用pci_enable_device_io()
变体。 这两种变体的不同之处在于,pci_enable_device_mem()
将仅初始化内存映射条,而pci_enable_device_io()
将初始化 I/O 条。 请注意,如果设备被多次启用,则每个操作都会递增struct pci_dev
结构中的.enable_cnt
字段,但只有第一个操作才会真正作用于该设备。
当要禁用 PCI 设备时,无论使用哪种启用变量,都应采用pci_disable_device()
方法。 该方法向系统发出系统不再使用 PCI 设备的信号。 以下是它的原型:
void pci_disable_device(struct pci_dev *dev)
pci_disable_device()
如果激活,还会禁用器件上的总线主控。 但是,直到pci_enable_device()
(或其变体之一)的所有调用方都调用了pci_disable_device()
,该设备才会被禁用。
根据定义,PCI 设备可以在其成为总线主控器的时刻启动总线上的事务。 设备启用后,您可能需要启用总线主控。
这实际上包括通过设置适当配置寄存器中的总线主控位在设备中启用 DMA。 PCI 内核为此提供了pci_set_master()
。 此方法实际上还调用pci_bios (pcibios_set_master()
,以便执行必要的特定于 Arch 的设置。 pci_clear_master()
将通过清除总线主控位来禁用 DMA。 这是相反的操作:
void pci_set_master(struct pci_dev *dev)
void pci_clear_master(struct pci_dev *dev)
请注意,如果设备打算执行 DMA 操作,则必须调用pci_set_master()
。
一旦设备绑定到驱动并由驱动启用后,访问设备内存空间就很常见了。 通常最先访问的是配置空间。 传统的 PCI 和 PCI-X 模式 1 设备具有 256 字节的配置空间。 PCI-X 模式 2 和 PCIe 设备有 4096 字节的配置空间。 驱动能够访问设备配置空间,或者读取驱动正常操作所必需的信息,或者设置一些重要参数,这是最基本的。 内核为不同大小的数据配置空间公开标准和专用 API(读写)。
为了从设备配置空间读取数据,您可以使用以下原语:
int pci_read_config_byte(struct pci_dev *dev, int where, u8 *val);
int pci_read_config_word(struct pci_dev *dev, int where, u16 *val);
int pci_read_config_dword(struct pci_dev *dev, int where, u32 *val);
上述代码分别读取由dev
参数表示的 PCI 设备的配置空间中的一个、两个或四个字节。 将read
值返回给val
参数。 在将数据写入设备配置空间时,您可以使用以下原语:
int pci_write_config_byte(struct pci_dev *dev, int where, u8 val);
int pci_write_config_word(struct pci_dev *dev, int where, u16 val);
int pci_write_config_dword(struct pci_dev *dev, int where, u32 val);
上述原语分别将一个、两个或四个字节写入设备配置空间。 val
参数表示要写入的值。
在读取或写入情况下,where
参数是距配置空间开头的字节偏移量。 但是,内核中存在一些经常访问的配置偏移量,这些配置偏移量由include/uapi/linux/pci_regs.h
中定义的符号命名宏来标识。 以下是简短的摘录:
#define PCI_VENDOR_ID 0x00 /* 16 bits */
#define PCI_DEVICE_ID 0x02 /* 16 bits */
#define PCI_STATUS 0x06 /* 16 bits */
#define PCI_CLASS_REVISION 0x08 /* High 24 bits are class, low 8 revision */
#define PCI_REVISION_ID 0x08 /* Revision ID */
#define PCI_CLASS_PROG 0x09 /* Reg. Level Programming Interface */
#define PCI_CLASS_DEVICE 0x0a /* Device class */
[...]
因此,要获取给定 PCI 设备的修订 ID,可以使用以下示例:
static unsigned char foo_get_revision(struct pci_dev *dev)
{
u8 revision;
pci_read_config_byte(dev, PCI_REVISION_ID, &revision);
return revision;
}
在上面,我们使用pci_read_config_byte()
,因为修订只由一个字节表示。
重要音符
由于数据以低位序格式存储在 PCI 设备中(或从 PCI 设备读取),读原语(实际上是word
和dword
变体)负责将读取的数据转换为 CPU 的本机字符顺序,而 WRite 原语(word
和dword
变体)负责在将数据写入设备之前将数据从原生 CPU 字节顺序转换为低位序。
内存寄存器几乎用于其他所有事情,例如,用于突发事务。 这些寄存器实际上对应于设备内存条。 然后,它们中的每一个被从系统地址空间分配一个存储器区域,使得对这些区域的任何访问都被重定向到相应的设备,目标是对应于 BAR 的正确的本地(在设备中)存储器。 这是内存映射 I/O。
在 Linux 内核内存映射的 I/O 世界中,在为内存区域创建映射之前请求(实际上是声称)内存区域是很常见的。 您可以将request_mem_region()
和ioremap()
原语用于这两个目的。 以下是它们的原型:
struct resource *request_mem_region (unsigned long start,
unsigned long n, const char *name)
void iomem *ioremap(unsigned long phys_addr, unsigned long size);
request_mem_region()
是一种纯保留机制,不执行任何映射。 它依赖于这样一个事实:其他司机应该有礼貌,应该在轮到他们时呼叫request_mem_region()
,这将防止另一名司机与已经声明的内存区域重叠。 除非此调用成功返回,否则您不应映射或访问声明的区域。 在其参数中,name
表示要赋予资源的名称,start
表示应该为哪个地址创建映射,n
表示映射应该有多大。 要获取给定条形图的此信息,您可以使用pci_resource_start()
、pci_resource_len()
甚至pci_resource_end()
,它们的原型如下所示:
unsigned long pci_resource_start (struct pci_dev *dev, int bar)
:此函数返回与索引为 BAR 的 BAR 关联的第一个地址(内存地址或 I/O 端口号)。unsigned long pci_resource_len (struct pci_dev *dev, int bar)
:此函数返回条形bar
的大小。unsigned long pci_resource_end (struct pci_dev *dev, int bar)
:此函数返回属于 I/O 区域编号bar
的最后一个地址。unsigned long pci_resource_flags (struct pci_dev *dev, int bar)
:此功能不仅与内存资源栏有关。 它实际上返回与此资源相关联的标志。IORESOURCE_IO
表示 BARbar
是 I/O 资源(因此适用于 I/O 映射 I/O),而IORESOURCE_MEM
表示它是内存资源(用于内存映射 I/O)。
另一方面,ioremap()
确实创建了实际映射,并在映射区域上返回内存映射的 I/O cookie。 作为示例,以下代码显示如何映射给定设备的bar0
:
unsigned long bar0_base; unsigned long bar0_size;
void iomem *bar0_map_membase;
/* Get the PCI Base Address Registers */
bar0_base = pci_resource_start(pdev, 0);
bar0_size = pci_resource_len(pdev, 0);
/* * think about managed version and use * devm_request_mem_regions() */
if (request_mem_region(bar0_base, bar0_size, "bar0-mapping")) {
/* there is an error */
goto err_disable;
}
/* Think about managed version and use devm_ioremap instead */ bar0_map_membase = ioremap(bar0_base, bar0_size);
if (!bar0_map_membase) {
/* error */
goto err_iomap;
}
/* Now we can use ioread32()/iowrite32() on bar0_map_membase*/
前面的代码可以很好地工作,但它很单调,因为我们会对每个栏执行此操作。 事实上,request_mem_region()
和ioremap()
是非常基本的原语。 PCI 框架提供了更多与 PCI 相关的功能,以便于执行以下常见任务:
int pci_request_region(struct pci_dev *pdev, int bar,
const char *res_name)
int pci_request_regions(struct pci_dev *pdev, const char *res_name)
void iomem *pci_iomap(struct pci_dev *dev, int bar,
unsigned long maxlen)
void iomem *pci_iomap_range(struct pci_dev *dev, int bar,
unsigned long offset, unsigned long maxlen)
void iomem *pci_ioremap_bar(struct pci_dev *pdev, int bar)
void pci_iounmap(struct pci_dev *dev, void iomem *addr)
void pci_release_regions(struct pci_dev *pdev)
前面的帮助器可以描述如下:
pci_request_regions()
将与pdev
PCI 设备关联的所有 PCI 区域标记为由所有者res_name
保留。 在其参数中,pdev
是要保留其资源的 PCI 设备,res_name
是要与资源关联的名称。pci_request_region()
则以bar
参数标识的单个条形为目标。pci_iomap()
为条形图创建映射。 您可以使用ioread*()
和iowrite*()
访问它。maxlen
指定要映射的最大长度。 如果您想在不先检查其长度的情况下访问完整的栏,请在此处传递0
。pci_iomap_range()
从条形图中的偏移量开始创建映射。 生成的映射从offset
开始,宽度为maxlen
。maxlen
指定要映射的最大长度。 如果要访问从offset
到末尾的完整栏,请在此处传递0
。pci_ioremap_bar()
提供一种防错方式(相对于pci_ioremap()
)来执行 PCI 内存重新映射。 它确保 BAR 实际上是内存资源,而不是 I/O 资源。 但是,它会映射整个条形图的大小。pci_iounmap()
与pci_iomap()
相反,后者会撤消映射。 它的addr
参数对应于先前由pci_iomap()
返回的 cookie。pci_release_regions()
与pci_request_regions()
相反。 它会释放先前声明(保留)的保留 PCI I/O 和内存资源。pci_release_region()
以单条变量为目标。
使用这些帮助器,我们可以重写与以前相同的代码,但这次是针对 bar1。 这将如下所示:
#define DRV_NAME "foo-drv"
void iomem *bar1_map_membase;
int err;
err = pci_request_regions(pci_dev, DRV_NAME);
if (err) {
/* an error occured */ goto error;
}
bar1_map_membase = pci_iomap(pdev, 1, 0);
if (!bar1_map_membase) {
/* an error occured */
goto err_iomap;
}
在声明并映射存储区之后,提供平台抽象的、ioread*()
和iowrite*()
API 访问映射的寄存器。
I/O 端口访问需要经历与 I/O 内存相同的步骤,尽管底层机制不同:请求 I/O 区域、映射 I/O 区域(这不是强制的,这只是礼貌问题),以及访问 I/O 区域。
前两个步骤已经在您没有注意到的情况下得到了解决。 实际上,pci_requestregion*()
原语同时处理 I/O 端口和 I/O 内存。 它依赖于资源标志(pci_resource_flags()
),以便为 I/O 端口调用适当的低级帮助器((request_region()
)或为 I/O 内存调用request_mem_region()
:
unsigned long flags = pci_resource_flags(pci_dev, bar);
if (flags & IORESOURCE_IO)
/* using request_region() */
else if (flag & IORESOURCE_MEM)
/* using request_mem_region() */
因此,无论资源是 I/O 内存还是 I/O 端口,您都可以安全地使用pci_request_regions()
或其单条变体pci_request_region()
。
这同样适用于 I/O 端口映射。 pci_iomap*()
原语能够处理 I/O 端口或 I/O 内存。 它们也依赖于资源标志,并且它们调用适当的帮助器来创建映射。 根据资源类型,底层映射函数是 I/O 存储器的ioremap()
,它们是IORESOURCE_MEM
类型的资源,以及 I/O 端口的__pci_ioport_map()
,它对应于IORESOURCE_IO
类型的资源。 __pci_ioport_map()
是一个依赖于 Arch 的函数(实际上被 MIPS 和 SH 架构覆盖),它在大多数情况下对应于ioport_map()
。
要确认我们刚才所说的内容,我们可以查看pci_iomap()
所依赖的pci_iomap_range()
函数体:
void iomem *pci_iomap_range(struct pci_dev *dev, int bar,
unsigned long offset, unsigned long maxlen)
{
resource_size_t start = pci_resource_start(dev, bar);
resource_size_t len = pci_resource_len(dev, bar);
unsigned long flags = pci_resource_flags(dev, bar);
if (len <= offset || !start)
return NULL;
len -= offset; start += offset;
if (maxlen && len > maxlen)
len = maxlen;
if (flags & IORESOURCE_IO)
return pci_ioport_map(dev, start, len);
if (flags & IORESOURCE_MEM)
return ioremap(start, len);
/* What? */
return NULL;
}
然而,当它访问 I/O 端口时,API 完全改变了。 以下是用于访问 I/O 端口的助手。 这些函数隐藏了底层映射的详细信息以及它们的类型。 下面列出了内核提供的用于访问 I/O 端口的函数:
u8 inb(unsigned long port);
u16 inw(unsigned long port);
u32 inl(unsigned long port);
void outb(u8 value, unsigned long port);
void outw(u16 value, unsigned long port);
void outl(u32 value, unsigned long port);
在前面的节选中,in*()
系列分别从port
位置读取一个、两个或四个字节。 获取的数据由一个值返回。 另一方面,out*()
系列在port
位置分别写入一个、两个或四个字节,称为value
参数。
需要为设备服务中断的驱动需要首先请求这些中断。 从probe()
方法中请求中断是很常见的。 也就是说,为了处理传统的和非 MSI IRQ,驱动可以直接使用pci_dev->irq
字段,该字段是在探测设备时预先分配的。
但是,对于更通用的方法,建议使用pci_alloc_irq_vectors()
API。 该函数定义如下:
int pci_alloc_irq_vectors(struct pci_dev *dev, unsigned int min_vecs,
unsigned int max_vecs, unsigned int flags);
如果成功,前面的函数将返回分配的向量数量(可能小于max_vecs
),如果出现错误,则返回负错误代码。 分配的向量的数量始终至少达到min_vecs
。 如果dev
可用的中断向量少于min_vecs
,则该功能将失败,并返回-ENOSPC
。
此功能的优势在于,它既可以处理传统中断,也可以处理 MSI 或 MSI-X 中断。 根据flags
参数,驱动可以指示 PCI 层为该设备设置 MSI 或 MSI-X 功能。 此参数用于指定设备和驱动使用的中断类型。 可能的标志在include/linux/pci.h
中定义:
PCI_IRQ_LEGACY
:单个传统 IRQ 矢量。PCI_IRQ_MSI
:在成功路径上,将pci_dev->msi_enabled
设置为1
。PCI_IRQ_MSIX
:在成功路径上,将pci_dev->msix_enabled
设置为1
。PCI_IRQ_ALL_TYPES
:这允许尝试分配上述任何类型的中断,但顺序固定。 总是先尝试 MSI-X 模式,如果成功,该功能会立即返回。 如果 MSI-X 失败,则会尝试 MSI。 当 MSI-X 和 MSI 都出现故障时,传统模式用作后备模式。 驱动可以依靠pci_dev->msi_enabled
和pci_dev->msix_enabled
来确定哪种模式是成功的。PCI_IRQ_AFFINITY
:这允许关联自动分配。 如果设置,pci_alloc_irq_vectors()
将在可用 CPU 之间分散中断。
要获取要传递给request_irq()
和free_irq()
的 Linux IRQ 号(对应于向量),请使用以下函数:
int pci_irq_vector(struct pci_dev *dev, unsigned int nr);
在前面的代码中,dev
是要操作的 PCI 设备,nr
是设备相关的中断向量索引(从 0 开始)。 现在,让我们更深入地看看该函数是如何工作的:
int pci_irq_vector(struct pci_dev *dev, unsigned int nr)
{
if (dev->msix_enabled) {
struct msi_desc *entry;
int i = 0;
for_each_pci_msi_entry(entry, dev) {
if (i == nr)
return entry->irq;
i++;
}
WARN_ON_ONCE(1);
return -EINVAL;
}
if (dev->msi_enabled) {
struct msi_desc *entry = first_pci_msi_entry(dev);
if (WARN_ON_ONCE(nr >= entry->nvec_used))
return -EINVAL;
} else {
if (WARN_ON_ONCE(nr > 0))
return -EINVAL;
}
return dev->irq + nr;
}
在前面的摘录中,我们可以看到 MSI-X 是第一次尝试(if (dev->msix_enabled)
)。 此外,返回的 IRQ 与设备探测时预先分配的原始pci_dev->irq
无关。 但如果启用了 MSI(dev->msi_enabled
为 TRUE),则此函数将执行一些健全性检查,并返回dev->irq + nr
。 这确认了这样一个事实:当我们在 MSI 模式下操作时,pci_dev->irq
被一个新值替换,并且这个新值对应于分配的 MSI 矢量的基本中断号。 最后,您会注意到没有对遗留模式进行特殊检查。
实际上,在传统模式下,预先分配的pci_dev->irq
保持不变,并且它只是一个分配的向量。 因此,在传统模式下操作时,nr
应为0
。 在本例中,返回的向量只有dev->irq
。
某些器件可能不支持使用传统线路中断,在这种情况下,驱动可以指定只接受 MSI 或 MSI-X:
nvec =
pci_alloc_irq_vectors(pdev, 1, nvec, PCI_IRQ_MSI | PCI_IRQ_MSIX);
if (nvec < 0)
goto out_err;
重要音符
请注意,MSI/MSI-X 和传统中断是互斥的,默认情况下,参考设计支持传统内部中断。 在设备上启用 MSI 或 MSI-X 中断后,它将保持此模式,直到它们再次被禁用。
PCI 总线类型(struct bus_type pci_bus_type
)的探针方法是pci_device_probe()
,在drivers/pci/pci-driver.c
中实现。 每次将新的 PCI 设备添加到总线或在系统中注册新的 PCI 驱动时,都会调用此方法。 此函数调用pci_assign_irq(pci_dev)
,然后调用pcibios_alloc_irq(pci_dev)
,以便将 IRQ 分配给 PCI 设备,即著名的pci_dev->irq
。 这个把戏开始发生在pci_assign_irq()
。 pci_assign_irq()
读取 PCI 设备所连接的引脚,如下所示:
u8 pin;
pci_read_config_byte(dev, PCI_INTERRUPT_PIN, &pin);
/* (1=INTA, 2=INTB, 3=INTD, 4=INTD) */
接下来的步骤依赖于 PCI 主桥,它的驱动应该公开许多回调,包括一个特殊的回调.map_irq
,它的目的是根据设备的插槽和之前读取的引脚为设备创建 IRQ 映射:
void pci_assign_irq(struct pci_dev *dev)
{
int irq = 0; u8 pin;
struct pci_host_bridge *hbrg = pci_find_host_bridge(dev->bus);
if (!(hbrg->map_irq)) {
pci_dbg(dev, "runtime IRQ mapping not provided by arch\n");
return;
}
pci_read_config_byte(dev, PCI_INTERRUPT_PIN, &pin);
if (pin) {
[...]
irq = (*(hbrg->map_irq))(dev, slot, pin);
if (irq == -1)
irq = 0;
}
dev->irq = irq;
pci_dbg(dev, "assign IRQ: got %d\n", dev->irq);
/* Always tell the device, so the driver knows what is the
* real IRQ to use; the device does not use it. */
pci_write_config_byte(dev, PCI_INTERRUPT_LINE, irq);
}
这是设备探测期间 IRQ 的第一次分配。 返回到pci_device_probe()
函数,在pci_assign_irq()
之后调用的下一个方法是pcibios_alloc_irq()
。 然而,在arch/arm64/kernel/pci.c
中,pcibios_alloc_irq()
被定义为一个弱而空的函数,仅被 AArch64 架构覆盖,它依赖于 ACPI(如果启用)来破坏分配的 IRQ。 也许在该功能中,其他体系结构也会想要覆盖此功能。
pci_device_probe()
的最终代码如下:
static int pci_device_probe(struct device *dev)
{
int error;
struct pci_dev *pci_dev = to_pci_dev(dev);
struct pci_driver *drv = to_pci_driver(dev->driver);
pci_assign_irq(pci_dev);
error = pcibios_alloc_irq(pci_dev);
if (error < 0)
return error;
pci_dev_get(pci_dev);
if (pci_device_can_probe(pci_dev)) {
error = pci_device_probe(drv, pci_dev);
if (error) {
pcibios_free_irq(pci_dev);
pci_dev_put(pci_dev);
}
}
return error;
}
重要音符
在调用pci_enable_device()
之前,PCI_INTERRUPT_LINE
中包含的 IRQ 值是错误的。 无论如何,外设驱动永远不应该改变PCI_INTERRUPT_LINE
b,因为它反映了 PCI 中断是如何连接到中断控制器的,这是不可改变的。
请注意,大多数处于传统 INTX 模式的 PCIe 设备将缺省为本地 INTA“虚拟线路输出”,这同样适用于通过 PCIe/PCI 网桥连接的许多物理 PCI 设备。 操作系统最终会在系统中的所有外围设备之间共享 INTA 输入;所有设备共享同一 IRQ 线路-我会让您想象一下灾难。
这个问题的解决方案是“虚拟线路 INTX IRQ swizzing”。 回到pci_device_probe()
函数的代码,它调用pci_assign_irq()
。 如果 y 你看这个函数 n(在drivers/pci/setup-irq.c
)的主体,你会注意到一些混乱的操作,这些操作旨在解决这个问题。
对于许多设备驱动来说,在中断处理程序中采用每个设备的自旋锁是很常见的。 由于中断在基于 Linux 的系统上保证是不可重入的,因此在处理基于管脚的中断或单个 MSI 时,没有必要禁用中断。 但是,如果设备使用多个中断,则驱动必须在锁定期间禁用中断。 如果设备发送不同的中断,其处理程序将尝试获取已被正在服务的中断锁定的自旋锁,这将防止死锁。 因此,在这种情况下使用的锁定原语是spin_lock_irqsave()
或spin_lock_irq()
,它们禁用本地中断并获取锁。 有关锁定原语和中断管理的更多详细信息,请参阅第 1 章,LIN 嵌入式开发人员的 UX 内核概念,。
有许多驱动仍在使用旧的、现在不推荐使用的 MSI 或 MSI-X API,它们是pci_enable_msi()
、pci_disable_msi()
、pci_enable_msix_range()
、pci_enable_msix_exact()
和pci_disable_msix()
。
前面列出的 API 根本不应该在新代码中使用。 但是,下面的代码摘录示例在 MSI 不可用时尝试使用 MSI 并回退到传统中断模式:
int err;
/* Try using MSI interrupts */
err = pci_enable_msi(pci_dev);
if (err)
goto intx;
err = devm_request_irq(&pci_dev->dev, pci_dev->irq,
my_msi_handler, 0, "foo-msi", priv);
if (err) {
pci_disable_msi(pci_dev);
goto intx;
}
return 0;
/* Try using legacy interrupts */
intx:
dev_warn(&pci_dev->dev,
"Unable to use MSI interrupts, falling back to legacy\n");
err = devm_request_irq(&pci_dev->dev, pci_dev->irq,
my_shared_handler, IRQF_SHARED, "foo-intx", priv);
if (err) {
dev_err(pci_dev->dev, "no usable interrupts\n");
return err;
}
return 0;
由于前面的代码包含不推荐使用的 API,因此将其转换为新的 API 可能是一个很好的练习。
既然我们已经完成了通用 PCI 设备驱动结构,并且已经解决了此类驱动中的中断管理问题,那么我们可以向前迈进一步,并利用设备的直接内存访问功能。
为了通过允许 CPU 不执行大量内存复制操作来加速数据传输和卸载 CPU,控制器和设备都可以配置为执行直接内存访问(DMA),这是一种在设备和主机之间交换数据而不涉及 CPU 的方法。 根据根联合体的不同,PCI 地址空间可以是 32 位或 64 位。
作为 DMA 传输的源或目标的系统内存区域称为 DMA 缓冲区。 但是,DMA 缓冲存储器范围取决于总线地址的大小。 这源于 24 位宽的 ISA 总线。 在这样的总线中,DMA 缓冲区只能位于系统内存的底部 16MB 中。 该底部存储器也称为ZONE_DMA
。 但是,PCI 总线没有这样的限制。 传统 PCI 总线支持 32 位寻址,而 PCIe 将其扩展到 64 位。 因此,可以使用两种不同的地址格式:32 位地址格式和 64 位地址格式。 为了拉取 DMA API,驱动应包含#include <linux/dma-mapping.h>
。
要通知内核支持 DMA 的缓冲区的任何特殊需要(包括指定总线宽度),可以使用定义如下的dma_set_mask()
:
dma_set_mask(struct device *dev, u64 mask);
这将有助于系统有效地分配内存,特别是如果设备可以直接寻址系统 RAM 中超过 4 GB 物理 RAM 的“一致内存”。 在上面的助手中,dev
是 PCI 设备的底层设备,mask
是要使用的实际掩码,您可以使用DMA_BIT_MASK
宏以及实际总线宽度来指定它。 dma_set_mask()
成功时返回0
。 任何其他值都表示发生错误。
以下是 32 位(或 64 位)位系统的示例:
int err = 0;
err = pci_set_dma_mask(pci_dev, DMA_BIT_MASK(32));
/*
* OR the below on a 64 bits system:
* err = pci_set_dma_mask(dev, DMA_BIT_MASK(64));
*/
if (err) {
dev_err(&pci_dev->dev,
"Required dma mask not supported, \
failed to initialize device\n");
goto err_disable_pci_dev;
}
也就是说,DMA 传输需要适当的内存映射。 该映射包括分配 DMA 缓冲器并为每个缓冲器生成总线地址,其类型为dma_addr_t
。 由于 I/O 设备通过总线控制器和任何介入的 I/O 存储器管理单元(IOMMU)的镜头查看 DMA 缓冲区,因此产生的总线地址将被提供给设备,以便通知它 DMA 缓冲区的位置。 由于每个内存映射还会生成一个虚拟地址,因此不仅会为映射生成总线地址,还会为映射生成虚拟地址。 为了使 CPU 能够访问缓冲区,DMA 服务例程还将 DMA 缓冲区的内核虚拟地址映射到总线地址。
有两种类型的(PCI)DMA 映射:一致性映射和字符串传输映射。 对于 either,内核提供了一个健康的 API,它屏蔽了处理 DMA 控制器的许多内部细节。
这种映射被称为一致,因为它为设备分配未缓存(一致)和无缓冲的内存以执行 DMA 操作。 由于设备或 CPU 的写入可以立即由任一方读取,而无需担心缓存一致性,因此此类映射也是同步的。 所有这些都使得一致的映射对于系统来说过于昂贵,尽管大多数设备都需要这样做。 但是,在代码方面,它更容易实现。
以下函数设置相干映射:
void * pci_alloc_consistent(struct pci_dev *hwdev, size_t size,
dma_addr_t *dma_handle)
如上所述,可以保证为映射分配的内存在物理上是连续的。 size
是您需要分配的区域的长度。 此函数返回两个值:可用于从 CPU 访问它的虚拟地址和第三个参数dma_handle
,它是一个输出参数,与函数调用为分配区域生成的总线地址相对应。 总线地址实际上就是您传递给 PCI 设备的地址。
请注意,pci_alloc_consistent()
实际上是设置了GFP_ATOMIC
标志的dma_alloc_coherent()
的哑包装器,这意味着分配不会休眠,从原子上下文中调用它是安全的。 如果您希望更改分配标志,则可能需要使用dma_alloc_coherent()
(强烈建议您这样做),例如,使用GFP_KERNEL
而不是 GFP_ATOMIC
。
请记住,映射的开销很大,它最少只能分配一个页面。 在幕后,它只分配 2 的幂的页数。页的顺序是用int order = get_order(size)
得到的。 这样的映射将用于持续设备生命周期的缓冲器。
要取消映射并释放这样的 DMA 区域,可以调用pci_free_consistent()
:
pci_free_consistent(dev, size, cpu_addr, dma_handle);
这里,cpu_addr
和dma_handle
对应于内核虚拟地址和由pci_alloc_consistent()
返回的总线地址。 虽然可以从原子上下文调用映射函数,但在这样的上下文中可能不会调用此函数。
还要注意,pci_free_consistent()
是dma_free_coherent()
的一个简单包装器,如果使用dma_alloc_coherent()
完成了映射,则可以使用它:
#define DMA_ADDR_OFFSET 0x14
#define DMA_REG_SIZE_OFFSET 0x32
[...]
int do_pci_dma (struct pci_dev *pci_dev, int direction, size_t count)
{
dma_addr_t dma_pa;
char *dma_va;
void iomem *dma_io;
/* should check errors */
dma_io = pci_iomap(dev, 2, 0);
dma_va = pci_alloc_consistent(&pci_dev->dev, count, &dma_pa);
if (!dma_va)
return -ENOMEM;
/* may need to clear allocated region */
memset(dma_va, 0, count);
/* set up the device */
iowrite8(CMD_DISABLE_DMA, dma_io + REG_CMD_OFFSET);
iowrite8(direction ? CMD_WR : CMD_RD);
/* Send bus address to the device */
iowrite32(dma_pa, dma_io + DMA_ADDR_OFFSET);
/* Send size to the device */
iowrite32(count, dma_io + DMA_REG_SIZE_OFFSET);
/* Start the operation */
iowrite8(CMD_ENABLE_DMA, dma_io + REG_CMD_OFFSET);
return 0;
}
前面的代码显示了如何执行 DMA 映射,并将 t 结果总线添加地址发送到设备。 在现实世界中,可能会引发中断。 然后,您应该从驱动内部处理它。
另一方面,流映射在代码方面有更多的限制。 首先,这样的映射需要来处理已经分配的缓冲区。 此外,已映射的缓冲区属于设备,而不再属于 CPU。 因此,在 CPU 可以使用缓冲区之前,应该先取消它的映射,以便解决可能的缓存问题。
如果需要启动写事务(CPU 到设备),驱动应该在映射之前将数据放在缓冲区中。 此外,必须指定数据应该移入的方向,并且数据应该仅基于此方向使用。
在 CPU 可以访问这些缓冲区之前,必须先取消它们的映射,这是因为缓存的缘故。 不用说,CPU 映射是可缓存的。 用于流式映射的dma_map_*()
系列函数(实际上由pci_map_*()
函数包装)将首先清除/使与缓冲区相关的缓存无效,并将依赖 CPU 在对应的dma_unmap_*()
(由pci_unmap_*()
函数包装)之前不访问这些缓冲区。 在 CPU 可以读取设备写入内存的任何数据之前,这些取消映射将再次使缓存无效(如有必要),以防在此期间发生任何推测性获取。 只有在此时,CPU 才能访问缓冲区。
有一些流映射可以接受几个不连续的和分散的缓冲区。 然后,我们可以列举两种形式的流映射:
- 单缓冲区映射,仅允许的单页映射
- 散布/聚集贴图,其中 ch 允许传递多个缓冲区(散布在内存上)
以下各节将介绍它们中的每一个。
这包括映射单个缓冲区。 它是用来偶尔绘制地图的。 也就是说,您可以使用以下内容设置单个缓冲区:
dma_addr_t pci_map_single(struct pci_dev *hwdev, void *ptr,
size_t size, int direction)
direction
应为PCI_DMA_BIDIRECTION
、PCI_DMA_TODEVICE
、PCI_DMA_FROMDEVICE
、or PCI_DMA_NONE
。 ptr
是缓冲区的内核虚拟地址,dma_addr_t
是可以发送到设备的返回总线地址。 您应该确保使用与数据的移动方式真正匹配的方向,而不是总是DMA_BIDIRECTIONAL
。 pci_map_single()
是dma_map_single()
的哑包装,其方向映射到DMA_TO_DEVICE
、DMA_FROM_DEVICE
或DMA_BIDIRECTIONAL
。
您应该使用以下内容释放映射:
Void pci_unmap_single(struct pci_dev *hwdev, dma_addr_t dma_addr,
size_t size, int direction)
这是对dma_unmap_single()
的包装。 dma_addr
应与pci_map_single()
返回的值相同(如果使用,则与dma_map_single()
返回的值相同)。 direction
和size
应该与您在映射中指定的内容相匹配。
下面显示了一个简化的流式映射示例(实际上是单个缓冲区):
int do_pci_dma (struct pci_dev *pci_dev, int direction,
void *buffer, size_t count)
{
dma_addr_t dma_pa;
/* bus address */
void iomem *dma_io;
/* should check errors */
dma_io = pci_iomap(dev, 2, 0);
dma_dir = (write ? DMA_TO_DEVICE : DMA_FROM_DEVICE);
dma_pa = pci_map_single(pci_dev, buffer, count, dma_dir);
if (!dma_va)
return -ENOMEM;
/* may need to clear allocated region */
memset(dma_va, 0, count);
/* set up the device */
iowrite8(CMD_DISABLE_DMA, dma_io + REG_CMD_OFFSET);
iowrite8(direction ? CMD_WR : CMD_RD);
/* Send bus address to the device */
iowrite32(dma_pa, dma_io + DMA_ADDR_OFFSET);
/* Send size to the device */
iowrite32(count, dma_io + DMA_REG_SIZE_OFFSET);
/* Start the operation */
iowrite8(CMD_ENABLE_DMA, dma_io + REG_CMD_OFFSET);
return 0;
}
在前面的示例中,假定已经分配了buffer
,并假定包含该数据。 然后对其进行映射,将其总线地址发送到设备,并开始 DMA 操作。 下面的代码示例(作为 DMA 事务的中断处理程序实现)演示了如何从 CPU 端处理缓冲区:
void pci_dma_interrupt(int irq, void *dev_id)
{
struct private_struct *priv = (struct private_struct *) dev_id;
/* Unmap the DMA buffer */
pci_unmap_single(priv->pci_dev, priv->dma_addr,
priv->dma_size, priv->dma_dir);
/* now it is safe to access the buffer */
[...]
}
在前面的内容中,映射在 CPU 可以使用缓冲区之前被释放。
散布/聚集映射是第二类流式 DMA 映射,使用它,您可以在单次拍摄中传输多个缓冲区(不一定在物理上是连续的),而不是分别映射每个缓冲区并逐个传输它们。 为了设置scatterlist
映射,您应该首先分配分散的缓冲区,这些缓冲区必须是页面大小的,但最后一个缓冲区的大小可能不同。 在此之后,您应该分配一个scatterlist
数组,并使用sg_set_buf()
用以前分配的缓冲区填充它。 最后,您必须在scatterlist
数组上调用dma_map_sg()
。 使用 DMA 后,调用数组上的dma_unmap_sg()
以取消映射scatterlist
项。
虽然您可以通过映射多个缓冲区的每个缓冲区,通过 DMA 逐个发送它们的内容,但 Scatter/Gather 可以通过将指向scatterlist
的指针连同长度(列表中条目的数量)一起发送到设备,从而一次发送所有内容:
u32 *wbuf1, *wbuf2, *wbuf3;
struct scatterlist sgl[3];
int num_mapped;
wbuf1 = kzalloc(PAGE_SIZE, GFP_DMA);
wbuf2 = kzalloc(PAGE_SIZE, GFP_DMA);
/* size may be different for the last entry */
wbuf3 = kzalloc(CUSTOM_SIZE, GFP_DMA);
sg_init_table(sg, 3);
sg_set_buf(&sgl[0], wbuf1, PAGE_SIZE);
sg_set_buf(&sgl[1], wbuf2, PAGE_SIZE);
sg_set_buf(&sgl[2], wbuf3, CUSTOM_SIZE);
num_mapped = pci_map_sg(NULL, sgl, 3, PCI_DMA_BIDIRECTIONAL);
首先,请注意pci_map_sg()
是dma_map_sg()
的愚蠢包装。 在前面的代码中,我们使用了sg_init_table()
,这会产生一个静态分配的表。 我们本可以使用sg_alloc_table()
进行动态分配。 此外,我们可以使用for_each_sg()
宏,以便在每个sg
(散布列表)元素上循环,同时使用sg_set_page()
帮助器来设置该散布列表绑定到的页面(您永远不应该直接分配该页面)。 以下是涉及此类帮助器的示例:
static int pci_map_memory(struct page **pages,
unsigned int num_entries,
struct sg_table *st)
{
struct scatterlist *sg;
int i;
if (sg_alloc_table(st, num_entries, GFP_KERNEL))
goto err;
for_each_sg(st->sgl, sg, num_entries, i)
sg_set_page(sg, pages[i], PAGE_SIZE, 0);
if (!pci_map_sg(priv.pcidev, st->sgl, st->nents,
PCI_DMA_BIDIRECTIONAL))
goto err;
return 0;
err:
sg_free_table(st);
return -ENOMEM;
}
在前面的块中,应该已经分配了页面,并且显然应该是PAGE_SIZE
大小。 st
是将在该函数的成功路径上适当设置的输出参数。
同样,请注意分散列表条目必须是页面大小的(除了最后一个条目,它可能有不同的大小)。 对于输入散列表中的每个缓冲区,dma_map_sg()
确定要分配给器件的正确总线地址。 每个缓冲区的总线地址和长度存储在结构分散列表条目中,但它们在结构中的位置因体系结构不同而不同。 因此,您可以使用两个宏来使代码可移植:
dma_addr_t sg_dma_address(struct scatterlist *sg)
:这将从此散列表条目返回总线(DMA)地址。unsigned int sg_dma_len(struct scatterlist *sg)
:此参数返回此缓冲区的长度。
dma_map_sg()
和dma_unmap_sg()
负责高速缓存一致性。 但是,如果必须在 DMA 传输之间访问(读/写)数据,则必须以适当的方式在每次传输之间同步缓冲区,如果 CPU 需要访问缓冲区,则使用dma_sync_sg_for_cpu()
;如果是需要访问的设备,则使用dma_sync_sg_for_device()
。 用于单个区域映射的类似函数是dma_sync_single_for_cpu()
和dma_sync_single_for_device()
。
考虑到以上所有的,我们可以得出结论:相干映射编码简单,但是使用成本很高,而流映射则具有相反的特性。 当 I/O 设备长时间拥有缓冲区时,使用流映射。 当每个 DMA 在不同的缓冲区(例如网络驱动)上操作时,流式 DMA 对于异步操作是常见的,在该缓冲区中,每个skbuf
数据被动态映射和取消映射。 但是,设备可能对您的应该使用什么方法拥有最终决定权。 也就是说,如果可以选择的话,您应该在可以的时候使用流式映射,在必须的时候使用连贯的映射。
在本章中,我们讨论了 PCI 规范总线和实现,以及它在 Linux 内核中的支持。 我们了解了枚举过程以及 Linux 内核如何允许访问不同的地址空间。 然后,我们按照详细的分步指南介绍了如何编写 PCI 设备驱动,从设备表填充到模块的exit
方法。 我们更深入地研究了中断机制及其基本行为,以及它们之间的差异。 现在您可以自己编写 PCI 设备驱动了,并且熟悉了它们的枚举过程。 此外,您了解它们的中断机制,并了解它们之间的差异(MSI 或非 MSI)。 最后,您了解了如何访问它们各自的内存区域。
在下一章中,我们将讨论 NVMEM 框架,它有助于为 EEPROM 等非易失性存储设备开发驱动。 这将有助于结束我们到目前为止在学习 PCI 设备驱动时所经历的复杂性。