免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 1185 | 回复: 0
打印 上一主题 下一主题

第 15 章 外设总线综述 [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2006-09-14 23:42 |只看该作者 |倒序浏览

第 15 章  外设总线综述
第 8 章介绍了最底层的硬件控制,本章将综述高级总线的结构。总线不仅构成了电气接口,同时还定义了编程接口。本章将重点讨论编程接口。
本章涉及到若干不同的总线结构,但讨论的重点是用于访问 PCI 外设的内核函数,这是因为 PCI 总线是当今普遍使用在桌面以及大型计算机上的外设总线,而且在内核中也得到了最好的支持。虽然 ISA 总线是一种“裸金属”类型的总线,但对某些电子爱好者来说,ISA 仍然很常用,因此将在本章后面讨论。当然,除了我们在第 8 章和第 9 章中所提到的内容以外,其实也没有多少需要对 ISA 总线进行讨论的内容。
15.1  PCI 接口
尽管许多计算机用户将 PCI(Peripheral Component Interconnect,外设组件互连接)看成是一种布置电子线路的方式,但实际上,它是一组完整的规范,定义了计算机的各种不同部分之间应该如何交互。
PCI 规范涵盖了计算机接口相关的大部分问题。我们不会在这里讲述所有的内容,这个小节将主要集中于 PCI 驱动程序寻找其硬件的方法,以及如何获得对 PCI 设备的访问。第 2 章的“自动和手工配置”以及第 9 章的“自动检测 IRQ 号”中所提到的探测技术,也可用于 PCI 设备,但是 PCI 规范提供了一种更好的探测方法。
PCI 设计为 ISA 标准的替代品,它有三个目标:在计算机和外设之间传输数据时,能够有更好的性能;能够尽量独立于平台;简化向/从系统中添加/删除外设的工作。
通过使用比 ISA 更高的时钟频率,PCI 总线获得了更好的性能。它的时钟频率一般在 25 到 33 MHz 范围(实际的频率取决于系统时钟),最新的总线可达到 66 MHz,甚至 133 MHz。另外,PCI 总线具有 32 位的数据总线,而且规范中还包括有 64 位的扩展(当然只有 64 位平台才会实现 64 位的数据总线)。平台无关性通常也是计算机总线的一个设计目标,对 PCI 来讲,平台无关性尤其重要,这是因为 PC 世界以往总是由一些处理器特有的接口标准所控制。目前,PCI 总线广泛应用于 IA-32、Alpha、PowerPC、SPARC64 和 IA-64 系统中,其它一些平台也使用了 PCI 总线。
驱动程序编写者常常头疼的问题是接口板的自动检测。PCI 设备是无跳线设备(不象某些老式外设),可在引导阶段自动配置。这样,设备驱动程序必须能够访问设备中的配置信息以便完成初始化。对 PCI 设备,这些工作无需探测就能完成。
15.1.1  PCI 寻址
每个 PCI 外设由一个总线编号、一个设备编号及一个功能编号标识。PCI 规范允许一个系统能够拥有高达 256 个总线,每个总线上可存在 32 个设备,而每个设备也可以是多功能板(比如音频设备外加 CD-ROM 驱动器),最多可达 8 种功能。每种功能在硬件级由一个 16 位的地址(或键)标识。为 Linux 编写的设备驱动程序,无需处理这些二进制的地址,因为它们可使用一种特殊的数据结构,称为 pci_dev 来访问设备。(我们在第 13 章看到过这个 struct pci_dev 结构。)
最近的工作站一般配置有至少两个 PCI 总线。在单个系统中插入多个总线,可通过“桥”来完成。桥是用来连接两个总线的特殊 PCI 外设。PCI 系统的整体布局组织为树型,其中每个总线连接到上一级总线,直到 0 号总线。CardBus PC 卡系统也通过桥连接到 PCI 系统上。典型的 PCI 系统可见图 15-1,其中标记出了各个不同的桥。


图 15-1:典型 PCI 系统的布局
尽管和 PCI 外设关联的 16 位硬件地址通常隐藏在 struct pci_dev 对象中,但有时仍然可见,尤其在需要列出正在使用的设备时。比如,在 lspci(pciutils 包的一个组件,大多数发行版中含有这个包)的输出,以及 /proc/pci 和 /proc/bus/pci 的布局信息中*。
注:注意这里的讨论是基于 2.4 内核版本的,向后兼容性问题在本章后面讨论。
在显示硬件地址时,有时显示为一个 16 位的值,有时显示为两个值(一个 8 位的总线编号和一个 8 位的设备及功能编号),有时显示为三个值(总线、设备和功能)。所有的值通常都显示为 16 进制数。
例如,/proc/bus/pci/devices 使用单个 16 位字段(便于分析及排序),而 /proc/bus/busnumber 将地址划分成了三个字段。下面说明了地址出现的方式,注意只列出了输出行的前面几行:
rudo% lspci | cut -d: -f1-2
00:00.0 Host bridge
00:01.0 PCI bridge
00:07.0 ISA bridge
00:07.1 IDE interface
00:07.3 Bridge
00:07.4 USB Controller
00:09.0 SCSI storage controller
00:0b.0 Multimedia video controller
01:05.0 VGA compatible controller
rudo% cat /proc/bus/pci/devices | cut -d\        -f1,3
0000    0
0008    0
0038    0
0039    0
003b    0
003c    b
0048    a
0058    b
0128    a
这两个设备清单以相同的顺序排列,因为 lspci 使用 /proc 文件作为其信息来源。拿 VGA video controller(VGA 视频控制器)作为例子,将 0x128 划分为总线(8位)、设备(5 位)及功能(3 位)时,可表示成 01:05.0。上述清单中的前后第二个字段分别表示了设备类型和中断号。
每个外设板的硬件电路对如下三种地址空间的查询进行应答:内存位置、I/O 端口以及配置寄存器。前两类地址空间由同一 PCI 总线上的所有设备共享(也就是说,在访问内存位置时,所有的设备将在同一时间看到该总线周期)。另一方面,配置空间利用了“位置寻址(geographical addressing)”。配置事务(亦即,总线对配置空间的访问)每次只会对一个 PCI 槽寻址,这样,在配置访问期间,根本不会发生任何冲突。
对驱动程序而言,内存和 I/O 区域通过通常的方式,即 inb 和 readb 等等进行访问。另一方面,配置事务却通过调用特定的内核函数来访问配置寄存器。关于中断,每个 PCI 槽有四个中断引脚,每个设备功能可使用其中的一个,而不用考虑这些引脚如何连接到 CPU。到 CPU 的中断连接由计算机平台负责,一般在 PCI 总线之外实现。因为 PCI 规范要求中断线是可共享的,因此,尽管处理器可能会限制 IRQ 线的数量(比如 x86),但仍然可以安装许多 PCI 接口板(每个接口板均有四个中断引脚)。
PCI 总线中的 I/O 空间使用 32 位地址总线(因此可有 4GB 个端口),而内存空间可通过 32 位或 64 位地址访问。但是,64 位地址仅仅在几个平台上可用。通常假定地址对设备是唯一的,但是软件可能会错误地将两个设备配置成相同的地址,导致无法访问这两个设备。但是,如果驱动程序不去访问那些不应该访问的寄存器,这样的问题就不会发生。另外,由接口板提供的每个内存和 I/O 地址区域,都可通过配置事务进行重新映射。这样,固件在系统引导时初始化 PCI 硬件,并将每个区域映射到不同的地址以避免冲突。*这些区域当前的映射情况,可从配置空间中读取,因此,Linux 驱动程序不需要探测就能访问其设备。在读取配置寄存器之后,驱动程序就可以安全访问其硬件。
注:实际上,配置过程并不限于系统引导阶段。比如热插拔设备,不在引导阶段出现,而会在后来出现。这里强调的是,设备驱动程序无需修改 I/O 或内存区域地址。
PCI 配置空间由 256 个字节组成,每个设备功能有一个配置空间,而且配置寄存器的布局是标准化的。配置空间的 4 个字节含有唯一的功能 ID,因此,驱动程序可通过查询外设的特定 ID 来标识其设备。*
注:我们可从设备的硬件手册中查到其 ID。文件 pci.ids 中包含有一个清单,该文件是 pciutils 包以及内核源代码的一部分。该文件并不打算包含完整的清单,但其中已列出了大部分有名的生产商和设备。
总之,我们可以独立地检索每个设备板的配置寄存器,这些寄存器中的信息可用来执行通常的 I/O 访问,而无需其它的“位置寻址(geographic addressing)”。
到此应该清楚的是,PCI 接口标准在 ISA 之上的主要创新,在于配置地址空间。因此,除了通常的驱动程序代码之外,PCI 驱动程序还应该有能力访问配置空间,而无需冒险进行探测。
本章其余内容中,我们将使用“设备”一词来表示一种设备功能,因为我们可以将多功能板上的每个功能看成是一个独立的入口。我们谈到设备时,表示的是一组“总线编号、设备编号、功能编号”,它们可由一个 16 位数或者两个 8 位数(通常称为 bus 和 devfn)来表示。
15.1.2  引导阶段
为了解 PCI 的工作原理,我们需要从系统引导开始讲起,因为这是配置设备的阶段。
当 PCI 设备上电时,硬件保持未激活状态。换句话说,该设备只会对配置事务做出响应。上电时,不会有内存和 I/O 端口映射到计算机的地址空间,其它设备相关功能,比如中断报告,也被禁止。
幸运的是,每个 PCI 主板均配备有能够处理 PCI 总线的固件,称为 BIOS、NVRAM 或 PROM(这取决于平台)。固件通过读写 PCI 控制器中的寄存器,提供了对设备配置地址空间的访问。
系统引导时,固件(或者 Linux 内核,如果经过配置的话)在每个 PCI 外设上执行配置事务,以便为设备的每个地址区域分配一个安全的位置。在驱动程序访问设备的时候,设备的内存和 I/O 区域已经映射到了处理器的地址空间。驱动程序可以修改默认的配置,但几乎没有任何理由需要这样做。
我们讲到,用户可以读取 /proc/bus/pci/devices 和 /proc/bus/pci/*/* 来了解 PCI 设备清单以及设备的配置寄存器。前者是个文本文件,包含有十六进制的设备信息,而后者是若干二进制文件,包含了每个设备的配置寄存器信息,每个文件对应一个设备。
15.1.3  配置寄存器和初始化
先前提到,配置空间的布局是设备无关的。本节我们将看到用来标识外设的配置寄存器。
PCI 设备配备有一个 256 字节的地址空间。前 64 字节是标准化的,其后的字节是设备相关的。图 15-2 给出了设备无关的配置空间。


图 15-2:标准化的 PCI 配置寄存器
如图所示,某些 PCI 配置寄存器是必需的,而某些是可选的。每个 PCI 设备要在必需的寄存器中包含有效值,而可选寄存器中的内容依赖于外设的实际功能。可选字段通常无用,除非必需字段表明它们是有效的。这样,必需的字段表明板子的功能,其中包括其它字段是否有用的信息。
值得注意的是,PCI 寄存器始终是 little-endian 的。尽管该标准设计为体系结构无关的,但 PCI 设计者仍然有点偏好 PC 环境。驱动程序编写者在访问多字节的配置寄存器时,要十分注意字节序。能够在 PC 上工作的代码到其它平台上,就可能无法工作。Linux 开发人员已经注意到了字节序问题(见下面一节“访问配置空间”),但是这个问题还是应该牢记心中。如果需要将系统固有字节序转换成 PCI 字节序,或者相反,则可以借助定义在  中的函数,这些函数在第 10 章中介绍,注意 PCI 字节序是 little-endian 的。
对这些配置项的描述已经超过了本书讨论的范围。通常,和设备一同发布的技术文档会详细描述已支持的寄存器。我们所关心的是,驱动程序如何查询设备,以及如何访问设备的配置空间。
用三个或五个 PCI 寄存器可标识一个设备:vendorID、deviceID 和 class 是常用的三个寄存器。每个 PCI 生产商会将正确的值赋于上述三个只读的寄存器,驱动程序可利用它们查询设备。另外,有时生产商会利用 subsystem vendorID 和 subsystem deviceID 两个字段进一步区分相似设备。
下面是这些寄存器的详细介绍。
vendorID
这是一个 16 位的寄存器,用于标识硬件制造商。例如,每个 Intel 设备被标识为同一个生产商编号,即 0x8086。PCI Special Interset Group 维护有一个全球的生产商编号注册表,制造商必须申请一个唯一编号并赋于该寄存器。
deviceID
这是另外一个 16 位寄存器,由制造商选择,而无需对设备 ID 进行官方注册。该 ID 通常和制造商 ID 配对生成一个唯一的 32 位硬件设备标识符。我们使用签名(signature)来表示一对制造商和设备 ID。设备驱动程序通常依靠该签名来标识其设备。我们可从硬件手册中找到目标设备的签名值。
class
每个外部设备属于某个“类(class)”。class 寄存器是一个 16 位的值,其中高 8 位标识了“基类(base class)”,或者组。例如,“ethernet(以太网)”和“token ring(令牌环)”是同属“network(网络)”组的两个类,而“serial(串行)”和“parallel(并行)”类同属“communication(通讯)”组。某些驱动程序可支持多个相似的设备,每个设备具有不同的签名,却属于同一个类。这种驱动程序可依靠 class 寄存器来标识它们的外设(如后所述)。
subsystem vendorID
subsystem deviceID
这两个字段可用来进一步标识设备。如果设备中的芯片是连接到本地(onboard)总线上的一个通用接口芯片,则可能会用于完全不同的多种用途,这时,驱动程序必须标识它所关心的实际设备。子系统(subsystem)标识符就用在这种场合。
使用这些标识符,我们可以检测并访问设备。在 2.4 内核中,已经引入了 PCI 驱动程序的概念,以及专用的初始化接口。新的驱动程序应该优先选择这个接口,然而,老的内核版本却无法使用。另外,还可以使用下面的头文件、宏和函数作为 PCI 驱动程序的接口,PCI 模块可使用它们查询它们的硬件设备。我们首先介绍向后兼容的接口,是因为这些接口能够移植到本书提到的所有内核版本。另外,这个接口还具有更加贴近直接的硬件管理、而较少抽象的优点。
#include  
驱动程序需要了解内核是否具备 PCI 功能。包含该头文件后,驱动程序可访问 CONFIG_ 宏,包括下面要讲到的 CONFIG_PCI。但要注意的是,每个包含  的源文件,都已经包含了这个头文件。
CONFIG_PCI
如果内核包含有对 PCI 调用的支持,则定义这个宏。并不是所有的计算机都有 PCI 总线,所以内核开发者选择将 PCI 支持做为一个编译选项,以便在非 PCI 计算机上运行 Linux 时节省内存。如果没有定义 CONFIG_PCI,每个 PCI 函数调用将返回一个错误状态,因此,驱动程序既可以使用预编译条件,也可以不使用预编译条件。如果驱动程序只能处理 PCI 设备(与同时具有 PCI 实现和非 PCI 实现相反),则应该在这个宏未定义时,产生一个编译错误。
#include  
这个头文件声明了本节介绍的所有原型,以及和 PCI 寄存器和位相关的符号名称。我们应该始终包含该头文件。这个头文件还包含有函数所返回的错误码的符号值。
int pci_present(void);
因为 PCI 相关函数不能在非 PCI 计算机上正常工作,因此,可利用 pci_present 检查 PCI 功能是否可用。在 2.4 中,不鼓励使用这个函数,因为它将检查是否存在某些 PCI 设备。但在 2.0 中,驱动程序必须调用这个函数,以避免在查询设备时出现错误。新近内核只会报告是否存在 PCI 设备。如果主机中存在 PCI 设备,则该函数返回布尔值真(非零)。
struct pci_dev;
该数据结构作为表示 PCI 设备的软件对象。它是系统每一个 PCI 操作的核心。
struct pci_dev *pci_find_device (unsigned int vendor, unsigned int device, const struct pci_dev *from);
如果定义有 CONFIG_PCI,而且 pci_present 返回真,则可利用该函数扫描已安装的设备链表,以查询具有特定签名的设备。from 参数用来得到具有相同签名的多个设备。该参数会指向已发现的最后一个设备,这样,下一个搜索就可以从这个位置开始,而无需从链表头开始。为了找到第一个设备,可将 from 指定为 NULL。如果找不到设备,返回 NULL。
struct pci_dev *pci_find_class (unsigned int class, const struct pci_dev *from);
该函数类似前一个函数,但它查询的是属于特定类(16 位的类,含有基类和子类值)的设备。除了非常底层的 PCI 驱动程序以外,现在该函数已经很少使用。from 参数和 pci_find_device 中的用法一样。
int pci_enable_device (struct pci_dev *dev);
该函数真正使能指定的设备。它激活该设备,在某些情况下,同时赋于其中断号和 I/O 区域。例如,在使能 CardBus 设备(在驱动程序级,它完全等价于 PCI)时将发生后面一种情况。
struct pci_dev *pci_find_slot (unsigned int bus, unsigned int devfn);
该函数根据一对总线/设备返回一个 PCI 设备结构。devfn 参数表示设备和功能两个项。该函数很少用到(驱动程序无需关心设备插入哪个 PCI 槽),在这里列出它,只是出于完整性考虑。
译者注:pci_find_slot 只在 2.4.x 版本中存在。
根据以上内容,处理单个设备类型的典型驱动程序,其初始化过程应该类似下面的代码。这段代码用于一个假设设备 jail(Just Another Instruction List):
#ifndef CONFIG_PCI
#  error "This driver needs PCI support to be available"
#endif
int jail_find_all_devices(void)
{
   struct pci_dev *dev = NULL;
   int found;
   if (!pci_present())
   return -ENODEV;
   for (found=0; found
jail_init_one 函数是设备特有的,所以没有在这里列出来。尽管如此,在编写上述函数时,有若干需要特别注意的事项:

  • 该函数需要执行一些额外的探测,以确保该设备是真正由驱动程序所支持的。某些 PCI 外设包含一个通用的 PCI 接口芯片以及设备特有的电路。所有使用相同接口芯片的外设板具有相同的签名。可通过读取子系统标识符,或设备特有的寄存器(在设备 I/O 区域中,将在后面介绍)执行进一步探测。
  • 在访问任意设备资源(I/O 区域或者中断)之前,驱动程序必须调用 pci_enable_device。如果要执行上面所说的额外探测,则该函数必须在探测发生之前调用。
  • 网络接口驱动程序应该将 dev->driver_data 指向与该接口关联的 struct net_device 结构。

上述摘录代码中的函数在拒绝该设备时返回 0,而在接受该设备时返回 1(可能还要根据进一步探测的结果决定返回值)。
上述代码在驱动程序只处理一种类型的 PCI 设备(由 JAIL_VENDRO 和 JAIL_ID 标识)时是正确的。如果需要支持更多 vendor/device 对,则最好使用后面“硬件抽象”一节中介绍的技术,除非要支持 2.4 版本之前的内核――这种情况下,可使用 pci_find_class。
使用 pci_find_class 要求 jail_find_all_devices 执行其它一些工作,它应该检查匹配 vendor/device 对的新设备(使用 dev->vendor 和 dev->device)。代码类似如下:
struct devid {unsigned short vendor, device} devlist[] = {
   {JAIL_VENDOR1, JAIL_DEVICE1},
   {JAIL_VENDOR2, JAIL_DEVICE2},
   /* ... */
   { 0, 0 }
};
   /* ... */
   for (found=0; found vendor; idptr++) {
          if (dev->vendor != idptr->vendor) continue;
          if (dev->device != idptr->device) continue;
          break;
       }
       if (!idptr->vendor) continue; /* not one of ours */
       jail_init_one(dev); /* device-specific initialization */
       found++;
   }
15.1.4  访问配置空间
在驱动程序检测到设备之后,它通常需要读取或写入三个地址空间:内存、端口和配置。对驱动程序而言,对配置空间的访问尤其不可缺少,这是因为对配置空间的访问,是找到设备内存和 I/O 空间映射结果的唯一途径。
因为处理器没有任何直接访问配置空间的途径,因此,需要计算机生产商来提供这个途径。为了访问配置空间,CPU 必须读取或写入 PCI 控制器的寄存器,但具体的实现依赖于计算机生产商,和我们这里的讨论无关,这是因为 Linux 提供了访问配置空间的标准接口。
对驱动程序,可通过 8 位、16 位或 32 位的数据传输访问配置空间,相关函数的原型定义在  中:
int pci_read_config_byte(struct pci_dev *dev, int where, u8 *ptr);
int pci_read_config_word(struct pci_dev *dev, int where, u16 *ptr);
int pci_read_config_dword(struct pci_dev *dev, int where, u32 *ptr);
从 dev 标识的设备配置空间中读入一个、两个或四个字节。where 参数是从配置空间起始位置计算的字节偏移量。从配置空间获得的值通过 ptr 返回,函数本身的返回值是错误码。word 和 dword 函数会将读取到的 little-endian 值转换成处理器固有的字节顺序,因此,我们自己无需关心字节序问题。
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);
向配置空间写入一个、两个或四个字节。和上面的函数一样,dev 标识设备,要写入的值通过 val 传递。word 和 dword 函数在把值写入外设之前,会将其转换成 little-endian 字节序。
读取配置变量的首选方法,是使用设备对应的 struct pci_dev 结构中的成员。虽然如此,如果需要写入并读取某个配置变量,我们仍然需要上述的函数。同时,如果需要和 2.4 版本之前的内核保持兼容,也需要使用 pci_read_ 函数。*在使用 pci_read_ 函数时,定位配置变量的最好方法是利用定义在  中的符号名称。例如,下面的函数调用检索设备的修订(revision)ID,注意为 pci_read_config_byte 函数传递了符号名称:
注:struct pci_dev 中的成员名称在 2.2 和 2.4 之间发生了一些变化,因为 2.2 中的设计存在一些不足。对 2.0,根本没有 pci_dev 结构,我们可利用的是由 pci-compat.h 头文件提供的一种简单模拟。
unsigned char jail_get_revision(unsigned char bus, unsigned char fn)
{
   unsigned char *revision;
   pci_read_config_byte(bus, fn, PCI_REVISION_ID, &revision);
   return revision;
}
译者注:这里给出的 pci_read_config_byte 函数用法,和前面讲述的原型不同,而和 2.4 版本前 pcibios_read_config_byte 函数的原型一致。
我们曾提到,以单个字节的形式访问多字节值时,我们必须正确处理字节序问题。
配置空间示例
如果要浏览系统中 PCI 设备的配置空间,则可以选择两种途径之一。比较简单的方法是利用 Linux 通过 /proc/bus/pci 提供的资源(2.0 版的内核不提供这种支持)。另外一个是我们这里介绍的方法,就是自己编写一些代码来完成该任务。该代码能够在已知所有的 2.x 内核版本上运行,并且也是分析 PCI 设备工作情况的一个好途径。源文件 pci/pcidata.c 包含在 O'Reilly FTP 站点提供的示例代码中。
该模块建立了一个动态的 /proc/pcidata 文件,其中包含了 PCI 设备配置空间的二进制快照。该快照在每次读取这个文件时更新。/proc/pcidata 的大小限制在 PAGE_SIZE 字节(为避免处理多页的 /proc 文件,相关内容可见第 4 章的“使用 /proc 文件系统”)。因此,该文件只能列出前 PAGE_SIZE/256 个设备的配置空间内容,依赖于所运行的平台,这个数字可能是 16 或者 32。我们选择 /proc/pcidata 为二进制文件,而不是类似其它 /proc 文件那样的文本格式,只是为了让代码简单一些。需要注意的是,/proc/bus/pci 中的文件也是二进制的。
pcidata 的另一个限制是它仅仅扫描系统中的第一个 PCI 总线。如果计算机含有到其它 PCI 总线的桥,pcidata 将忽略这些总线。这对示例代码来讲,并不是个大问题。
出现在 /proc/pcidata 中的设备的顺序,和 /proc/bus/pci/devices 使用的顺序一样(但和版本 2.0 中的 /proc/pci 所使用的顺序相反)。
例如,我们的帧捕获器在 /proc/pcidata 中位于第五,其配置寄存器内容(当前的)如下:
morgana% dd bs=256 skip=4 count=1 if=/proc/pcidata | od -Ax -t x1
1+0 records in
1+0 records out
000000 86 80 23 12 06 00 00 02 00 00 00 04 00 20 00 00
000010 00 00 00 f1 00 00 00 00 00 00 00 00 00 00 00 00
000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
000030 00 00 00 00 00 00 00 00 00 00 00 00 0a 01 00 00
000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
*
000100
上述转储结构中的数字表示 PCI 寄存器中的值。参考图 15-2,我们可以了解每个数字的含义。另外,我们也可以使用 pcidump 程序(也可从 FTP 站点获得),它可以将上述数字格式化并利用标记输出。
pcidump 的代码不值得在这里列出,因为其中包含有一个大表,以及扫描这个表的 10 多行代码。但是,我们给出这个程序的若干输出(节录):
morgana% dd bs=256 skip=4 count=1 if=/proc/pcidata | ./pcidump
1+0 records in
1+0 records out
       Compulsory registers:
Vendor id: 8086
Device id: 1223
I/O space enabled: n
Memory enabled: y
Master enabled: y
Revision id (decimal): 0
Programmer Interface: 00
Class of device: 0400
Header type: 00
Multi function device: n
       Optional registers:
Base Address 0: f1000000
Base Address 0 Is I/O: n
Base Address 0 is 64-bits: n
Base Address 0 is below-1M: n
Base Address 0 is prefetchable: n
Does generate interrupts: y
Interrupt line (decimal): 10
Interrupt pin (decimal): 1
pcidata、pcidump 以及 grep 工具,可用来调试驱动程序的初始化代码。当然,这些工具完成的任务,部分可从 pciutils 包中获得,这个包包含在新近发布的所有 Linux 发行版中。还需注意的是,和本书其它的代码不同,pcidata.c 模块遵循 GPL,这是因为我们从内核源代码的 PCI 扫描循环中拿了一些代码。作为驱动程序编写者,这应该不是个问题,因为这个模块只是作为一个支持工具,而不是一个可重复利用的新驱动程序模板。
15.1.5  访问 I/O 和内存空间
一个 PCI 设备可实现多达 6 个 I/O 地址区域。每个区域可以是内存也可以是 I/O 位置区间。大多数设备在内存区域实现 I/O 寄存器,因为这是一个通用的明智选择(详细情况,可参阅第 8 章的“I/O 端口和 I/O 内存”)。但是,不象通常的内存,I/O 寄存器不应该由 CPU 缓存,因为对这种寄存器的每次访问可能存在副作用(side effect)。将 I/O 寄存器实现为内存区域的 PCI 设备,通过在配置寄存器中设置“(内存是可预取的)memory-is-prefetchable”标志而标记这个不同。*
注:该信息保存在 PCI 寄存器基地址的低位上,这些位在  中有定义。
如果内存区域被标记为可预取(prefetchable),则 CPU 可缓存其内容,并实现所有的优化。另一方面,非可预取的(nonprefetchable)内存访问不能被优化,因为每个访问可能具有副作用,尤其是通常的 I/O 端口。PCI 设备通常将映射到内存地址区间的控制寄存器标记为非可预取的,而将 PCI 板上类似显示内存这样的东西,标记为可预取的。在本节,我们使用“区域”一词指代一般的 I/O 地址空间,不管是内存映射的,还是端口映射的。
一个接口板通过配置寄存器报告其区域的大小和当前位置――即图 15-2 中的 6 个 32 位寄存器,它的符号名称为 PCI_BASE_ADDRESS_O 到 PCI_BASE_ADDRESS_5。因为 PCI 定义的 I/O 空间是 32 位地址空间,因此,内存和 I/O 可使用相同的配置接口。如果设备使用 64 位的地址总线,则可为每个区域使用两个连续的 PCI_BASE_ADDRESS 寄存器(低位优先),来声明 64 位内存空间中的区域。对一个设备来讲,既可以提供 32 位区域,也可以提供 64 位区域。
Linux 2.4 中的 PCI I/O 资源
在 Linux 2.4 中,PCI 设备的 I/O 区域已经集成到了一般的资源管理。出于该原因,我们无需访问配置变量来获得设备内存和 I/O 空间的映射情况。获得区域信息的首选接口,由如下函数组成:
unsigned long pci_resource_start(struct pci_dev *dev, int bar);
该函数返回六个 PCI I/O 区域之一的第一个地址(内存地址或 I/O 端口编号)。该区域由整数的 bar(base address register,基地址寄存器)决定,可取 0 到 5 的值。
unsigned long pci_resource_end(struct pci_dev *dev, int bar);
该函数返回第 bar 个 I/O 区域的后一个地址。注意这是最后一个可用的地址,而不是该区域之后的第一个地址。
unsigned long pci_resource_flags(struct pci_dev *dev, int bar);
该函数返回资源关联的标志。
资源标志用来定义单个资源的某些特性。对与 PCI I/O 区域关联的 PCI 资源,该信息从基地址寄存器中获得,但对其它与 PCI 设备无关的资源,它可能来自任何地方。
所有的资源标志定义在  中,下面列出其中最重要的几个:
IORESOURCE_IO
IORESOURCE_MEM
如果对应的 I/O 区域存在,将设置上面标志中的一个,而且只有一个。
IORESOURCE_PREFETCH
IORESOURCE_READONLY
上述标志定义内存区域是可预取的,或者是写保护的。对 PCI 资源来讲,从来不会设置后面的那个标志。
通过使用 pci_resource_ 函数,设备驱动程序可完全忽略底层的 PCI 寄存器,因为系统已经使用这些寄存器构建了资源信息。
基地址寄存器
通过避免对 PCI 寄存器的访问,我们可以获得更好的硬件抽象,以及向前的兼容性,但却不能获得向后兼容性。如果希望自己的设备驱动程序能够在 2.4 之前的内核上工作,就不能使用上述这些漂亮的资源接口,而必须直接访问 PCI 寄存器。
在这个小节,我们将讨论基地址寄存器的工作方式,以及访问这些寄存器的方法。如果读者可以直接利用先前讲述过的资源管理函数,这里的内容则有点多余。
我们不会在这里详细地介绍基地址寄存器,这是因为,如果我们正准备编写一个 PCI 驱动程序,则无论如何都会有设备的硬件手册可作参考。尤其是,我们不会使用寄存器的可预取位,以及两个“类型”位,而且我们的讨论会限制在 32 位外设上。但是,这些东西的实现方法,以及 Linux 驱动程序处理 PCI 内存的方法,仍然值得一看。
PCI 规范指出,制造商必须将每个有效区域映射到可配置地址。这意味着,设备必须为每个区域装备一个可编程的 32 位地址解码器,利用了 64 位 PCI 扩展的任何 PCI 板,都应该有 64 位的可编程解码器。
实际的实现以及可编程解码器的使用,因如下事实而简化:一个区域中的字节数,通常是 2 的幂,例如 32 字节、4 KB 或者 2MB 等等。另外,也无需考虑将区域映射到不对齐地址的情况,1MB 的区域会对齐在 1MB 倍数的地址处,而 32 字节的区域会在 32 的倍数处对齐。PCI 规范利用了这种对齐,它指出地址解码器只需看到地址总线上的高位,而且只有高位是可编程的。这种约定也意味着任意区域的大小都必须是 2 的幂。
将一个 PCI 区域映射到物理地址空间的工作,通过在配置寄存器高位中设置适当的值而实现。例如,对 1 MB 区域,它有 20 位的地址空间,通过设置寄存器的高 12 位来进行重新映射,这样,为了让 PCI 板具有 64 MB 到 65MB 的地址范围,可向寄存器写入 0x040xxxxx 范围的任意一个地址。实际情况下,只有很高的地址才用来映射 PCI 区域。
“部分解码”技术还有一个附加的有点,软件可以检查配置寄存器中的非可编程位数来判断 PCI 区域的大小。为此,PCI 标准指出不用的位必须在读取时始终为 0。标准还要求 I/O 区域的最小大小是 8 字节,而内存区域的最小范围是 16 字节,这样,就可以在基地址寄存器的低位中保存一些额外的信息:

  • 位 0 位“空间(space)”位。如果区域映射到内存地址空间,则设置为 0;如果映射到 I/O 地址空间,则设置为 1。
  • 位 1 和 2 是“类型(type)”位:内存区域可标记为 32 位区域、64 位区域或者“必须映射到 1 MB 以下的 32 位区域”(已废弃的 x86 特有类型,现在已不再使用)。
  • 位 3 是“可预取(prefetchable)”位,用于内存区域。

读到这里,读者应该知道资源标志的来源了。
检测 PCI 区域的大小,可利用  中定义的若干位掩码来简化:如果是内存区域,则 PCI_BASE_ADDRESS_SPACE 位掩码被设置为 PCI_BASE_ADDRESS_SPACE_MEMORY,是 I/O 区域时,设置为 PCI_BASE_ADDRESS_IO。若要知道映射后内存区域的实际地址,可将 PCI 寄存器和 PCI_BASE_ADDRESS_MEM_MASK 进行“与”操作,以便丢弃前面那些低位值。对 I/O 寄存器,应使用 PCI_BASE_ADDRESS_IO_MASK。需要注意的是,设备制造商可能以任意顺序使用 PCI 区域。使用第一个和第三个区域,却留下第二个区域不用的设备,还是很常见的。
下面将给出报告 PCI 区域当前位置和大小的典型代码。这段代码是 pciregions 模块的一个部分,和 pcidata 在同一目录中发布。该模块建立一个 /proc/pciregions 文件,并使用先前给出的代码生成数据。该程序将一个全 1 的值写入配置寄存器,然后读取这些值,以便了解寄存器中哪些位可被编程。注意当这个程序探测配置寄存器时,设备实际被映射到了物理地址空间的顶端,这就是探测过程中禁止中断报告的原因(这样可在将区域映射到错误的地点时,避免驱动程序访问该区域)。
尽管 PCI 规范指出 I/O 地址空间是 32 位宽,但某些制造商明显倾向于 x86 平台,假设它为 64 KB,而不会实现基地址寄存器的所有 32 位。这就是下面的代码(以及内核)忽略 I/O 区域地址掩码高位的原因。
static u32 addresses[] = {
   PCI_BASE_ADDRESS_0,
   PCI_BASE_ADDRESS_1,
   PCI_BASE_ADDRESS_2,
   PCI_BASE_ADDRESS_3,
   PCI_BASE_ADDRESS_4,
   PCI_BASE_ADDRESS_5,
   0
};
int pciregions_read_proc(char *buf, char **start, off_t offset,
                  int len, int *eof, void *data)
{
   /* this macro helps in keeping the following lines short */
#define PRINTF(fmt, args...) sprintf(buf+len, fmt, ## args)
   len=0;
   /* Loop through the devices (code not printed in the book) */
       /* Print the address regions of this device */
       for (i=0; addresses[ i]; i++) {
           u32 curr, mask, size;
           char *type;
           pci_read_config_dword(dev, addresses[ i],&curr);
           cli();
           pci_write_config_dword(dev, addresses[ i],~0);
           pci_read_config_dword(dev, addresses[ i],&mask);
           pci_write_config_dword(dev, addresses[ i],curr);
           sti();
           if (!mask)
               continue; /* there may be other regions */
           /*
            * apply the I/O or memory mask to current position.
            * note that I/O is limited to 0xffff, and 64-bit is not
            * supported by this simple implementation
            */
           if (curr & PCI_BASE_ADDRESS_SPACE_IO) {
               curr &= PCI_BASE_ADDRESS_IO_MASK;
           } else {
               curr &= PCI_BASE_ADDRESS_MEM_MASK;
           }
           len += PRINTF("\tregion %i: mask 0x%08lx, now at 0x%08lx\n",
                       i, (unsigned long)mask,
                          (unsigned long)curr);
           /* extract the type, and the programmable bits */
           if (mask & PCI_BASE_ADDRESS_SPACE_IO) {
               type = "I/O"; mask &= PCI_BASE_ADDRESS_IO_MASK;
               size = (~mask + 1) & 0xffff; /* Bleah */
           } else {
               type = "mem"; mask &= PCI_BASE_ADDRESS_MEM_MASK;
               size = ~mask + 1;
           }
           len += PRINTF("\tregion %i: type %s, size %i (%i%s)\n", i,
                         type, size,
                         (size & 0xfffff) == 0 ? size >> 20 :
                           (size & 0x3ff) == 0 ? size >> 10 : size,
                         (size & 0xfffff) == 0 ? "MB" :
                           (size & 0x3ff) == 0 ? "KB" : "B");
           if (len > PAGE_SIZE / 2) {
               len += PRINTF("... more info skipped ...\n");
               *eof = 1; return len;
           }
       }
   return len;
}
下面是 /proc/pciregions 给出的帧捕获器的区域报告:
Bus 0, device 13, fun  0 (id 8086-1223)
       region 0: mask 0xfffff000, now at 0xf1000000
       region 0: type mem, size 4096 (4KB)
值得注意的是,该程序所报告的内存大小有时会有点夸大事实。例如,/proc/pciregions 报告某个显卡有 16 MB 的内存,但实际上只有 1 MB。这是可接受的,因为这个大小信息仅仅被固件用来分配地址范围。对了解设备细节的驱动程序编写者来说,超过实际情况的大小并不是问题,他们能够正确处理固件赋于的地址范围。在这种情况下,无需修改 PCI 寄存器,就可在设备上添加更多的 RAM。
如果存在这种夸大的情况,也会反映在资源接口中,这时,pci_resource_size 将报告夸大后的大小。
15.1.6  PCI 中断
PCI 很容易处理中断。在 Linux 的引导阶段,计算机固件已经为设备赋于一个唯一的中断号,驱动程序只需使用该中断号即可。中断号保存在配置寄存器 60 (PCI_INTERRUPT_LINE)中,该寄存器为一个字节宽。这允许多达 256 个中断线,实际的中断线数量受到 CPU 的限制。驱动程序无需检测中断号,因为从 PCI_INTERRUPT_LINE 中找到的值是保证正确的。
如果设备不支持中断,寄存器 61(PCI_INTERRUPT_PIN)是 0,否则为非零。但是,因为驱动程序知道自己的设备是否是中断驱动的,因此,通常不需要读取 PCI_INTERRUPT_PIN 寄存器。
这样,处理中断的 PCI 相关代码仅仅需要读取配置字节,以便获得中断号,如下代码所示。否则,要利用第 9 章的内容。
result = pci_read_config_byte(dev, PCI_INTERRUPT_LINE, &myirq);
if (result) { /* deal with error */ }
本节其余内容为好奇的读者提供了一些附加信息,但这些信息对编写驱动程序没有多少帮助。
PCI 连接头有四个中断引脚,外设板可使用其中任意一个。每个引脚被独立连接到主板的中断控制器,因此,中断可被共享,而不会出现任何电气问题。然后,中断控制器负责将中断线(引脚)映射到处理器硬件。这一依赖于平台的操作由控制器完成,这样,总线本身可以获得平台无关性。
位于 PCI_INTERRUPT_PIN 的只读配置寄存器用来告诉计算机,实际使用的是哪个引脚。要注意每个设备板可拥有 8 个设备,而每个设备使用单独的中断引脚,并在自己的配置寄存器报告引脚的使用情况。同一设备板上的不同设备可使用不同的中断引脚,或者共享同一个中断引脚。
另一方面,PCI_INTERRUPT_LINE 寄存器是可读/写的。在计算机的引导阶段,固件扫描其 PCI 设备,并根据每个 PCI 槽的中断引脚连接情况设置每个设备的寄存器。这个值由固件赋于,是因为只有固件知道主板如何将不同的中断引脚连接至处理器。但是,对设备驱动程序,PCI_INTERRUPT_LINE 是只读的。有意思的是,新近的 Linux 内核在某些情况下,无需借助 BIOS 就可以分配中断线。
15.1.7  处理热插拔设备
在 2.3 开发周期中,内核开发人员检查了 PCI 编程接口,以便简化接口并支持热插拔设备,也就是说,某些设备可在系统运行时添加或删除(比如 CardBus 设备)。本节介绍的内容并不适合 2.2 和更早的内核,但对新驱动程序来讲,应该是首选的处理方法。
这里的基本思想是,在系统生命周期中,无论何时出现一个新的设备,所有可用的设备驱动程序都必须检查新设备,以判断新设备是否属于自己。因此,能够处理热插拔设备的驱动程序必须在内核中注册一个对象,而不是使用经典的 init 和 cleanup 入口点。该对象的 probe 函数将用来检查系统中的设备,其结果是,要么接管该设备,要么不予考虑。
这种方法会一直工作下去:通常情况下,系统会在引导期间扫描一次静态的设备清单,如果不存在对应的设备,模块化的驱动程序将被卸载,而监视总线的外部进程将在需要时再次装载这些驱动程序。这就是 PCMCIA 子系统以前的工作方式,现在已经集成到内核当中,从而可以利用类似的方法在其它不同的硬件环境下处理类似的问题。
但是读者也许会提出反对意见,即可热插拔的 PCI 设备现在不太常见。然而,新的驱动程序对象技术也可以对那些需要处理大量不同设备的非热插拔驱动程序提供很大帮助。初始化代码可被简化并流线化,从而只需根据一个已知设备清单来检查“当前”的设备,而无需主动查询 PCI 总线,这种方法需要循环调用 pci_find_class 一次,或循环调用 pci_find_device 多次。
让我们分析下面的代码。这段代码利用了  中定义的 pci_driver,该结构定义了它所实现的操作,并包含有它所支持的设备清单(为避免对其代码的多余调用)。一句话,下面的代码表明如何针对一个假想的“热插拔 PCI 模块(hot plug PCI module,HPPM)”进行初始化和清除处理。
struct pci_driver hppm_driver = { /* .... */ };
int hppm_init_module(void)
{
   return pci_module_init(&hppm_driver);
}
int hppm_cleanup_module(void)
{
   pci_unregister_driver(&hppm_driver);
}
module_init(hppm);
module_exit(hppm);
读者已经看到,整个过程非常简单。内部细节隐藏在 pci_module_init 的实现、以及驱动程序内部结构之中。我们将自顶而下讲述相关函数:
int pci_register_driver(struct pci_driver *drv);
该函数将驱动程序插入到由系统维护的一个链表中。已编译的设备驱动程序利用该函数执行它们的初始化,模块化的代码不直接使用该函数。函数的返回值是由该驱动程序处理的设备个数。
int pci_module_init(struct pci_driver *drv);
该函数封装了上面那个函数,并提供给模块化的代码调用。当成功时返回 0,而在未发现设备时返回 -ENODEV。这样,就可以避免在无设备时仍将模块驻留在内存中(而在匹配的设备出现时,期望该模块能够被自动装载)。因为该函数定义为内嵌函数,所以它的行为在 MODULE 未被定义时会有所不同,因此,对非模块化的代码,该函数甚至可作为 pci_register_driver 的替代函数。
void pci_unregister_driver(struct pci_driver *drv);
该函数将指定的驱动程序从已知驱动程序链表中删除。
void pci_insert_device(struct pci_dev *dev, struct pci_bus *bus);
void pci_remove_device(struct pci_dev *dev);
这两个函数实现了热插拔系统的另一面。它们由事件处理器调用,而事件处理器和总线报告的插入/拔出事件相关联。
struct pci_driver *pci_dev_driver(const struct pci_dev *dev);
该工具函数查找与某个设备相关联的驱动程序(如果存在)。/proc/bus 的支持函数使用这个函数,但对设备驱动程序来讲,没有多少意义。
pci_driver 结构
pci_driver 数据结构是热插拔支持的核心结构,我们将详细描述这个数据结构。该结构其实很小,只有几个方法以及一个设备 ID 清单。
struct list_head node;
用来管理驱动程序链表。这个链表是第 10 章“链表”一节中介绍过的通用链表,对设备驱动程序来讲意义不大。
char *name;
驱动程序名称,日志消息中使用该名称。
const struct pci_device_id *id_table;
是个数组,其中列出了该驱动程序所支持的设备。当有设备和这个数组中列出的项匹配时,才会调用 probe 方法。如果该成员被指定为 NULL,将对系统中的所有设备调用 probe 函数。如果该成员不为 NULL,则数组中的最后一项必须设置为 0。
int (*probe)(struct pci_dev *dev, const struct pci_device_id *id);
该函数必须初始化传递进入的设备,并且在成功时返回 0,而在失败时返回负的错误码(实际上,错误码当前还没有被用到,但最好还是返回一个错误值,而不是 -1)。
void (*remove)(struct pci_dev *dev);
remove 方法用来告诉设备驱动程序,它应该关闭设备,并且停止对其的处理,释放任何关联的内存。该函数在两种情况下被调用:当设备从系统中移走时,或者当驱动程序调用 pci_unregister_driver 从系统中卸载时。和 probe 不同,这个方法是针对某个 PCI 设备的,而不是针对该驱动程序所处理的所有设备集合,该特定设备通过参数传递进入。
int (*suspend)(struct pci_dev *dev, u32 state);
int (*resume)(struct pci_dev *dev);
上述函数是 PCI 设备的电源管理函数。如果设备驱动程序支持电源管理功能,则应该实现这两个方法,以便关闭并激活设备。这些函数由高层代码在适当的时间调用。
PCI 驱动程序对象相当直接,而且好用。笔者认为无需对这些成员做进一步说明,因为通常的硬件处理代码能够很好地适合这些抽象函数。
现在唯一未作解释的是 struct pci_device_id 对象。该结果包含了若干 ID 成员,要驱动的实际设备必须匹配所有的成员。设置成 PCI_ANY_ID 的成员,告诉系统忽略对应的 ID。
unsigned int vendor, device;
该驱动程序感兴趣的设备的 vendor 和 device ID。这两个值分别和 PCI 配置空间中的 0x00 和 0x02 寄存器相匹配。
unsigned int subvendor, subdevice;
即子 ID,和 PCI 配置空间中的 0x2C 和 0x2E 寄存器匹配。因为有时一对 vendor/device ID 可能会标识一组设备,而驱动程序只能支持其中的一部分,所以要使用这两个子 ID 进行进一步的匹配。
unsigned int class, class_mask;
如果设备驱动程序要处理整个一个类,或者某个子集,就可以将前面的成员设置为 PCI_ANY_ID,同时使用 class 标识符。class_mask 的存在,可让驱动程序处理某个基类,或者只是其中的子类 。如果使用 vendor/device 标识符选择设备,则这两个成员都必须设置为 0(而不是 PCI_ANY_ID,因为相关的检查通过和掩码成员的逻辑与操作完成)。
unsigned long driver_data;
该成员留给设备驱动程序自己使用。举个例子,可利用该成员在编译阶段区别各个不同的设备,从而避免运行时冗长的条件判断。
值得注意的是,pci_device_id 数据结构只是提供给系统的一个暗示;实际的设备驱动程序仍然可以自由地从 probe 方法中返回非零,这样,既使设备和设备标识符数组中的某项匹配,也会拒绝该设备。举个例子,如果存在若干具有相同签名的设备,驱动程序可以在确定是否能够驱动该外设之前,进一步查询其它信息。
15.1.8  硬件抽象
到此为止,我们知道了系统是如何处理市场上各种各样的 PCI 控制器的,也已经完整讨论了 PCI 总线。本节将提供其它一些资料,以帮助感兴趣的读者了解一些内核是如何将面向对象的软件层扩展到最底层的硬件的。
用来抽象硬件的机制,就是包含方法的普通结构。这是一种强有力的技术,它在普通的函数调用开支之上,仅仅多了指针取值这样一点最小的开支。在 PCI 管理中,唯一依赖于硬件的操作是完成配置寄存器读取和写入的操作,而 PCI 领域中的其它任何工作,都是通过直接读取和写入 I/O 及内存地址空间完成的,这些工作,都可以在 CPU 的直接控制下完成。
为此,实现硬件抽象的相关结构,仅仅包含 6 个成员:
struct pci_ops {
   int (*read_byte)(struct pci_dev *, int where, u8 *val);
   int (*read_word)(struct pci_dev *, int where, u16 *val);
   int (*read_dword)(struct pci_dev *, int where, u32 *val);
   int (*write_byte)(struct pci_dev *, int where, u8 val);
   int (*write_word)(struct pci_dev *, int where, u16 val);
   int (*write_dword)(struct pci_dev *, int where, u32 val);
};
该结构在  中定义,并由 drivers/pci/pci.c 使用,后者定义了实际的公用函数。
处理 PCI 配置空间的 6 个函数,比起单个指针的取值操作来,要花费更多的开支,这是因为代码是高度面向对象的,所以使用了级联指针。但这个开支并不是一个问题,因为这些操作的执行次数非常少,而且也从来不会在速度关键的地方调用。例如,pci_read_config_byte 函数的实际实现将扩展为:
dev->bus->ops->read_byte();
系统中的各个 PCI 总线在引导阶段检测,这时,struct pci_bus 结构以及相关功能被建立,其中包括 ops 成员。
通过“硬件操作”数据结构实现硬件抽象,在 Linux 中很典型。一个重要的例子是 struct alpha_machine_vector 数据结构。该结构在  中定义,并用来处理各种 Alpha 计算机之间的不同。
15.2  回顾 ISA
ISA 总线在设计上相当老旧,并且其性能也很差,但是,它仍然占有很大一部分的扩展设备市场。如果要支持老主板,而速度不是非常重要时,ISA 比起 PCI 要占些优势。ISA 这个老标准的另外一个优点是,如果你是一位电子爱好者,则可以非常容易地设计开发自己的 ISA 设备,而如果要独自开发 PCI 设备,有时简直是不可能的。
另一方面,ISA 的最大不足在于它紧紧绑定在 PC 架构上,其接口总线拥有 80286 处理器的所有限制,从而经常让系统程序员头疼。ISA 的另外一个大问题(来自最初的 IBM PC),是缺少位置的寻址,从而导致许多问题,而且在添加新设备时,要不断修改跳线并测试。值得注意的是,最老的 Apple II 计算机都采用了位置寻址方法,从而可以装备无跳线的扩展板卡。
尽管 ISA 总线有如此大的缺点,但仍然应用于若干意想不到的领域。例如,MIPS 处理器的 VR41xx 系列,在几种掌上型电脑中装备有 ISA 兼容的扩展总线,但看起来完全不同。这种意想不到的应用,其背后的原因是某些基于 ISA 的传统硬件的成本非常低廉,比如基于 8390 的以太网卡,这样,利用 ISA 电气信号的 CPU 就能够非常容易地利用这种便宜的 PC 设备,尽管从设计上讲,这种接口非常糟糕。
15.2.1  硬件资源
一个 ISA 设备可配备有 I/O 端口、内存区域以及中断线。
尽管 x86 处理器支持 64 KB 的 I/O 端口地址(也就是说,处理器有 16 条地址线),但某些老式的 PC 硬件只能处理最低的 10 条地址线。这就将可用地址空间限制在 1024 个端口,因为任何只能处理低 10 位地址线的设备,都会错误地将 1KB 到 64KB 范围内的地址看成是低地址。某些外设巧妙地利用这一限制,将端口映射到了低 1KB 字节,而使用高地址线来选择不同的设备寄存器。例如,映射到 0x340 端口的设备,也可以安全使用 0x740、0xB40 等端口。
如果说可用的 I/O 端口受到限制,内存访问情况更加糟糕。ISA 设备只能使用 640 KB 和 1 MB 之间,以及 15 MB 和 16 MB 之间的内存。640 KB 到 1 MB 的范围由 PC BIOS、VGA 兼容适配器,以及其它各种设备使用,新设备能用的空间就非常有限了。另一方面,Linux 不直接支持 15 MB 处的内存访问,如果想通过修改内核来支持对该范围的内存访问,现在已经得不偿失了。
ISA 设备板可利用的第三个资源是中断线。连接到 ISA 总线的中断线非常有限,而且由所有的接口板卡共享。这样,如果设备未经正常配置,将出现多个不同设备使用同一中断线的结果。
尽管最初的 ISA 规范不允许在设备间共享中断,但大多数设备板都允许中断的共享。*软件方面的中断共享在第 9 章的“中断共享”中讲述。
注:中断共享的问题涉及到电气特性:如果某个设备通过低阻抗电平驱动信号线成为无效状态,则无法实现中断共享。另一方面,如果设备使用拉升电阻导致无效状态,则共享就是可能的。这是现在使用的标准。但是,因为 ISA 中断是边缘触发的,而不是电平触发的,因此,就有可能丢失中断信号。边缘触发的中断在硬件级别很容易实现,但却无法安全共享中断。
15.2.2  ISA 编程
对编程而言,内核和 BIOS 都可以无需任何帮助而访问 ISA 设备(这点上和 PCI 不同)。我们能利用的设施,只有 I/O 端口寄存器以及 IRQ 线,相关论述,可参阅“使用资源”(第 2 章)和“安装中断处理程序”(第 9 章)。
本书第一部分中讲述的所有编程技术都可以应用于 ISA 设备。驱动程序可以探测 I/O 端口,而中断线可利用第 9 章的“自动检测 IRQ 号”中描述的技术进行自动检测。
第 8 章的“使用 I/O 存储器”中简要介绍过辅助函数 isa_readb 以及其它相关函数,这里不再赘述。
15.2.3  即插即用规范
某些新的 ISA 设备板遵循特殊的设计规则,并且需要一个特殊的初始化序列,以便简化附加接口板的安装和配置。这一规范称为“即插即用(PnP)”,其中包括一堆建立和配置无跳线 ISA 设备的笨重规则。PnP 设备实现了 I/O 区域的重新分配,而 PC BIOS 负责重新分配(有点类似 PCI)。
简而言之,PnP 的目标就是为了获得类似 PCI 设备那样的灵活性,而无需修改底层的电气接口(即 ISA 总线)。为此,该规范定义了一组设备无关的配置寄存器,以及通过位置寻址接口板的方法――但物理总线并不支持各板独立的(位置相关的)连线,因为每个 ISA 信号线都会连接到每个插槽。
位置寻址通过赋于计算机中的每个 PnP 外设一个小整数,即“Card Select Number(CSN)”来工作。每个 PnP 设备配备有一个唯一的顺序标识号,有 64 位宽,并且硬编码到外设板中。CSN 赋值过程利用该唯一顺序标识号来标识 PnP 设备。但是,只能在引导阶段才能对 CSN 进行安全的赋值,因此,需要 BIOS 有能力量处理 PnP 设备。出于这个原因,老的计算机需要用户获得并插入一张特殊的配置磁盘,这样才能识别 PnP 设备。
遵循 PnP 规范的接口板在硬件上比较复杂。比起 PCI 板来,它们更为精细,而且要求更为复杂的软件。安装这些设备时,一样会遇到麻烦,既使安装很顺利,也仍然要面对性能限制,以及有限的 IAS 总线 I/O 空间等问题。因此,只要可能,应该尽量安装 PCI 设备。
如果读者对 PnP 配置软件感兴趣,可浏览 drivers/net/3c509.c,这个驱动程序的探测函数处理 PnP 设备。Linux 2.1.33 在 drivers/pnp 中添加了对 PnP 的初始支持,也可一看。
15.3  PC/104 和 PC/104+
在工业界,当前有两种非常流行的总线结构:PC/104 和 PC/104+,它们都是 PC 类单板计算机的标准。
这两个总线规定了印刷电路板的外形因素,以及板间互连的电气/机械规范。这种总线的真正好处在于,可以利用设备一面的插座连接器将多个电路板垂直堆集起来。
这两个总线的电子和逻辑布局分别和 ISA(PC/104)及 PCI(PC/104+)一样,因此,软件不会注意到它们和通常桌面总线之间的不同。
15.4  其它 PC 总线
PCI 和 ISA 是 PC 领域两种最常用的外设接口,但它们并不是唯一的 PC 总线。这里给出其它一些能在 PC 市场上找到的总线特点。
15.4.1  MCA
微通道结构(Micro Channel Architecture,MCA)是在 PS/2 计算机和某些笔记本电脑中使用的 IBM 标准。微通道的最主要问题是其文档很少见,从而导致到现在为止,Linux 中也没有对 MCA 的良好支持。
在硬件级别,微通道比起 ISA 来有许多特点。它支持多主体(multimaster)DMA、32 位地址和数据线、共享中断线,以及用来访问各板卡上配置寄存器的位置寻址等等。这些寄存器称为“Programmable Option Select(POS)”,但却没有 PCI 寄存器的所有特征。Linux 对微通道的支持包括一些模块可用的函数。
设备驱动程序可以读取整数值 MCA_bus,以便判断是否运行在微通道计算机上,这点和使用 pci_present 判断是否存在 PCI 设备非常类似。如果该符号是一个预处理宏,则会定义MCA_bus_ _is_a_macro 宏。如果 MCA_bus_ _is_a_macro 未被定义,则 MCA_bus 是一个整型变量,可由模块化代码访问。MCA_bus 和MCA_bus_ _is_a_macro 在  中定义。
15.4.2  EISA
扩展 ISA(EISA)总线是对 ISA 总线的 32 位扩展,同时具有兼容的接口连接器,也就是说,ISA 设备可以插入 ISA 连接器。附加的线路在 ISA 连接器之下走线。
类似 PCI 和 MCA,EISA 总线也为无跳线设备设计,并具有和 MCA 一样的特点:32 位地址和数据线、多主体 DMA,以及共享中断线。EISA 设备由软件配置,而不需要操作系统的任何特殊支持。Linux 内核中已经有一些 EISA 驱动程序,主要是以太网设备和 SCSI 控制器。
EISA 驱动程序检查 EISA_bus 的值判断是否存在 EISA 总线。和 MCA_bus 类似,EISA_bus 可以是宏,也可以是变量,取决于是否定义有 EISA_bus_ _is_a_macro 宏。这两个符号均定义在  中。
对驱动程序而言,内核中没有对 EISA 的特殊支持,而程序员必须自己处理 ISA 扩展。驱动程序使用标准的 EISA I/O 操作来访问 EISA 寄存器。内核中已有的驱动程序可作为参考样例。
15.4.3  VLB
另外一个对 ISA 的扩展是 VESA 局部总线(VESA Local Bus ,VLB)接口总线,这个总线将 ISA 连接器进行了扩展,添加了第三个纵向插座。设备可以插入这个额外的连接器(而不需要插入另外两个 ISA 连接器插槽),这是因为 VLB 插槽重复了 ISA 连接器中的所有重要信号。这种不使用 ISA 槽的“独立”的 VLB 外设很少见,因为大多数设备需要接触后面板,这样才能连接到外部连接器上。
比起 EISA、MCA 和 PCI 总线,VESA 总线在功能上有更多的限制,因此正在从市场上消失。对 VLB,内核中也不存在任何特殊支持。但是,Linux 2.0 中的 Lance Ethernet 驱动程序和 IDE 磁盘驱动程序可处理这些设备的 VLB 版本。
15.5  SBus
现在,大量计算机装备 PCI 或 ISA 接口总线的同时,大部分不太新的 SPARC 工作站却使用 SBus 连接它们的外设。
尽管 SBus 存在很长一段时间了,但它具有相当高级的设计。尽管只有 SPARC 计算机使用该总线,但它的初衷却是处理器无关,并针对 I/O 外设板进行了优化。换句话说,我们可以将额外的 RAM 插入 SBus 插槽(RAM 扩展板已经从 ISA 领域消失,而 PCI 根本不支持 RAM 扩展板)。这种优化可简化硬件设备和系统软件的设计,其代价是主板更加复杂一些。
SBus 总线的这种 I/O 处理方法导致外设使用“虚拟”地址来传输数据,以跳过分配连续 DMA 缓冲区的需求。主板负责将虚拟地址解码并映射到物理地址。这要求在总线上附加 MMU(内存管理单元),负责该任务的芯片称为“IOMMU”。与使用物理地址的接口总线相比,这种设计似乎有些复杂,但因为 SPARC 处理器始终将 MMU 核心从 CPU 核心中分离(要么是物理上,要么至少是概念上),而使之大大简化。实际上,这种设计上的选择也被其它巧妙的处理器设计共享,从而获得整体上的好处。这种总线的另外一个好处是,不需要在所有外设中实现地址解码器并处理地址冲突。
SBus 外设在它们的 PROM 中使用 Forth 语言来初始化它们自身。选择 Forth 的原因是,其解释器是轻量级的,因此可在所有计算系统中得以实现。另外,SBus 规范描述了引导过程,因此,遵循该规范的 I/O 设备能很容易地适合系统,并且在系统引导时得以识别。对支持多平台的设备来讲,这一步意义非凡,这完全不同于 PC 为中心的 ISA 领域。但是,因为许多商业原因,这个总线并未取得成功。
尽管当前内核版本对 SBus 设备提供相当完善的支持,但该总线已经很少用到,因此不值得在这里详述。感兴趣的读者可以查看 arch/space/kernel 和 arch/sparc/mm 中的源文件。
15.6  NuBus
另外一个已经被遗忘的接口总线是 NuBus,可在老式 Mac 计算机(使用 M68k 家族 CPU)中找到。
所有的总线都是内存映射的(类似 M68k 中的所有东西),而且设备只能通过位置寻址。这是 Apple 的象征,因为更老式的 Apple II 都已经具备类似的总线设计,这正是它的好处所在。不好的一面在于,几乎不太可能找到任何有关 NuBus 的文档,这归咎于 Apple 在 Mac 计算机上一贯遵循的封闭所有东西的策略(不同于先前的 Apple II 系统,其源代码和图表可以非常低的代价获得)。
drivers/nubus/nubus.c 包含了我们就该总线所知道的一切,读起来也相当有趣。读者可以从中看出,开发人员利用了多少艰难的反向工程方法。
15.7  外部总线
接口总线领域,最近出现了一个新的家族:外部总线。这包括 USB、FireWire、和 IEEE1284(基于并口的外部总线)。这些接口某种程度上和老式的、非外部的技术,比如 PCMCIA/CardBUS,甚至 SCSI 类似。
从概念上讲,这些总线既不是功能完整的接口总线(比如 PCI),也不是哑的通讯通道(比如串口)。很难对利用其功能的软件进行分类,通常可划分为两个级别:硬件控制器的驱动程序(比如针对 PCI SCSI 适配器的驱动程序,或者早先在“PCI 接口”中描述过的 PCI 控制器),以及针对特定“客户”设备的驱动程序(比如处理一般 SCSI 磁盘的 sd.c,以及处理插入总线的板卡的 PCI 驱动程序)。
但是还有其它一些问题。排除 USB,对它们的支持要么尚不成熟,要么需要修正(后面这种情况尤其符合 SCSI 内核子系统,许多最好的内核黑客都报告说它不太优化)。
15.7.1  USB
USB,即 Universal Serial Bus(一致串行总线),是唯一一种足够成熟的外部总线,因此值得作些讨论。从拓扑上讲,USB 子系统不能称为总线,它更象一棵由若干点对点线路组成的树。线路中包括四根线(地、电源,以及两个信号线),它将设备和集线器(和双绞线以太网类似)连接起来。通常,PC 类计算机提供一个“根集线器”,并提供两个连接插座。我们可以在插座上接入设备或者其它集线器。
在技术层面,USB 总线没有什么令人激动的地方,因为它其实是一种单主体实现,主机不停地轮询各种设备。尽管该总线有此固有限制,但它仍有一些有意思的功能特征,比如,设备能够为其数据传输请求一个固定带宽,以便可靠支持视频和音频 I/O。USB 的另外一个重要特征是,它仅仅作为设备和主机的通讯通道,而不需要它所传递的数据存在特定的含义或结构。*
注:实际上,仍然有一些结构,但大部分只是为了满足几个预先定义的设备类型的通讯需求,比如,键盘不会分配带宽,而摄像头需要。
在这点上,USB 不同于 SCSI 通讯方式,而类似标准的串行介质。
这些特征,以及 USB 的热插拔能力,使得 USB 成为一种便利的低成本机制,我们无需关闭计算机,打开盖子,甚至旋开螺丝钉,就可以将设备连接到计算机。USB 正在成为 PC 市场上的流行接口,但是尚不适合于高速设备,因为它的最大传输率是 12 Mb 每秒。
版本 2.2.18(及以后)、2.4.x 的内核支持 USB,所有计算机中的 USB 控制器属于两类之一,而这两类均包含在标准内核中。
15.7.2  编写 USB 驱动程序
对“客户”设备驱动程序而言,其驱动程序处理热插拔的方法和 pci_driver 方法类似:设备驱动程序在 USB 子系统中注册自己的驱动程序对象,其后使用 vendor 和 device 标识符来标识硬件的插入。
相关的数据结构是 struct usb_driver,典型用法如下:
#include  
static struct usb_driver sample_usb_driver = {
       name:        "sample",
       probe:       sample_probe,
       disconnect:  sample_disconnect,
};
int init_module(void)
{
   /* just register it; returns 0 or error code */
   return usb_register(&sample_usb_driver);
}
void cleanup_module(void)
{
   usb_deregister(&sample_usb_driver);
}
当新设备连接到系统中时(或者驱动程序被载入,而总线上已存在不明设备时),USB 内核子系统将调用该数据结构中声明的 probe 函数。
每个设备为系统提供 vendor、device 和 class 标识符来标识自己,这和 PCI 设备类似。sample_probe 的任务,就是查询它接收到的信息,并指出该设备是否属于自己。
如果设备属于自己,该函数返回一个非 NUUL 指针,该指针将用来标识该设备。通常是指向设备特有数据结构的指针,该结构处于设备驱动程序的核心地位。
为了和设备交换信息,需要告诉 USB 子系统如何通讯。这个任务通过填充一个 struct urb(表示 USB request block)结构,并将其传递给 usb_submit_urb 执行。这个步骤通常由与设备特殊文件关联的 open 方法或者等价的函数来完成。
注意并不是所有的 USB 驱动程序需要请求自己的主设备号,并实现它自己的设备特殊文件。如果某个设备属于内核已提供一般性支持的类型,则不必建立自己的设备文件,而需要通过其它途径报告信息。
一般性管理的一个例子是输入处理。如果 USB 设备是一个输入设备(比如绘图板),就不需要为这个设备分配一个主设备,而只需调用 input_register_device 注册该硬件。在这种情况下,输入设备的 open 回调函数负责调用 usb_submit_urb 建立通讯。
因此,USB 输入驱动程序必须依赖于其它几个系统部分,大部分驱动程序可作为模块。USB 输入设备驱动程序的模块结构见图 15-3。


图 15-3:与 USB 输入管理相关的模块
读者可从 O'Reilly FTP 站点上找到一个完整的 USB 设备驱动程序。它是一个很简单的键盘/鼠标驱动程序,只是为了说明如何设计一个完整的 USB 驱动程序。为了简单起见,该驱动程序并没有使用输入子系统来报告事件,而是使用 printk 打印消息。为测试该驱动程序,你需要准备一个 USB 键盘或鼠标。
现在有相当多的 USB 文档可获得,其中包括本书作者之一撰写的两篇论文,其风格和技术等级类似《Linux 驱动程序》。这些论文中甚至包含了更为完整的 USB 样例设备驱动程序,它使用了内核的输入子系统,如果没有 USB 设备,也可以通过其它途径得以运行。读者可在
http://www.linux.it/kerneldocs
上找到这两篇论文。
15.8  向后兼容性
当前内核中的 PCI 实现在版本 2.0 内核中并不存在。2.0 中的支持 API 非常原始,这是因为那时缺少本章描述过的各种对象。
早期版本中,我们可以使用访问配置空间的六个函数,它们接收 PCI 设备的 16 位低层键作为参数,而不使用指向 struct pci_dev 结构的指针。同时,我们必须在读写配置空间之前包含  头文件。
幸运的是,处理这些差异并不是个大问题,如果包含 sysdep.h 文件,则可以在 2.0 下使用与 2.4 一样的原语。版本 2.0 的 PCI 支持可从头文件 pci-compat.h 获得,在 2.0 下编译时,将自动被 sysdep.h 包含。pci-compat.h 实现了处理 PCI 总线的最重要函数。
如果你使用 pci-compat.h 开发能够在 2.0 到 2.4 的任意版本上运行的驱动程序,则必须在使用完 pci_dev 时调用 pci_release_device。这是因为这个头文件为 2.0 建立的伪 pci_dev 结构是用 kmalloc 分配的,而 2.2 和 2.4 内核的真正结构是内核中的静态资源。在 2.2 和 2.4 内核中编译时,sysdep.h 定义这个函数不做任何事情,这样就不会有任何害处。读者可以从 pciregions.c 或 pcidata.c 中看到实际的可移植代码。
另一个 2.0 的相关差异是 PCI 的 /proc 支持。2.0 中没有 /proc/bus/pci 文件层次(实际上根本没有 /proc/bus),而只有一个 /proc/pci 文件。该文件的内容是二进制的,对人来讲不可读。在 2.2 版本,我们可在编译阶段选择一个“向后兼容“的 /proc/pci,而在版本 2.4 中,该文件被彻底废弃。
热插拔 PCI 驱动程序概念(以及 struct pci_driver)在版本 2.4 中出现。我们没有针对老版本提供向后的兼容宏。
15.9  快速参考
和往常一样,这个小节总结本章出现的符号。
#include  
CONFIG_PCI
这个宏可用来对 PCI 代码进行条件编译。当一个 PCI 模块装载进入一个非 PCI 内核时,insmod 将说明有若干符号无法解析。
#include  
这个头文件包含 PCI 寄存器的符号名称,以及若干制造商和设备 ID 值。
int pci_present(void);
该函数返回一个布尔值,说明计算机是否存在 PCI 总线。
struct pci_dev;
struct pci_bus;
struct pci_driver;
struct pci_device_id;
这些结构表示涉及 PCI 管理的对象。pci_driver 对 Linux 2.4 来说是全新的,而 struct pci_device_id 是围绕 pci_driver 工作的。
struct pci_dev *pci_find_device(unsigned int vendor, unsigned int device, struct pci_dev *from);
struct pci_dev *pci_find_class(unsigned int class, struct pci_dev *from);
上述函数用来查询设备链表,以找出匹配给定签名或者属于某个特定类的设备。如果没有找到,该函数返回 NULL。from 可用来继续搜索。在第一次调用这些函数时,from 必须为 NULL,如果要再次搜索,from 必须指向刚刚找到的设备。
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);
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);
这些函数用来读取或者写入 PCI 配置寄存器。尽管 Linux 内核处理了字节序问题,但当我们从单个字节装配多字节值时,程序员必须小心处理字节序问题。PCI 总线是 little-endian。
int pci_register_driver(struct pci_driver *drv);
int pci_module_init(struct pci_driver *drv);
void pci_unregister_driver(struct pci_driver *drv);
这些函数支持 PCI 驱动程序的概念。预先编译的代码使用 pci_register_driver 函数(它返回该驱动程序管理的设备数目),而模块化的代码应该调用 pci_module_init 函数(该函数在系统存在一个和多个设备时返回 0,而当没有适合的设备插入系统时,返回 -ENODEV。
#include  
#include  
前一个头文件是 USB 相关信息所在的地方,因此,USB 设备驱动程序必须包含该文件。后一个定义了输入子系统的核心。Linux 2.0 中不存在这两个头文件。
struct usb_driver;
int usb_register(struct usb_driver *d);
void usb_deregister(struct usb_driver *d);
usb_driver 是 USB 设备驱动程序的主要部分。在模块装载和卸载阶段,必须进行注册和注销工作。

本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u/23470/showart_171398.html
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP