---
title: 第 11 章 PCI设备
prev: books/arch-handbook/isa
next: books/arch-handbook/scsi
showBookMenu: true
weight: 13
params:
  path: "/books/arch-handbook/pci/"
---

[[pci]]
= PCI设备
:doctype: book
:toc: macro
:toclevels: 1
:icons: font
:sectnums:
:sectnumlevels: 6
:sectnumoffset: 11
:partnums:
:source-highlighter: rouge
:experimental:
:images-path: books/arch-handbook/

ifdef::env-beastie[]
ifdef::backend-html5[]
:imagesdir: ../../../../images/{images-path}
endif::[]
ifndef::book[]
include::shared/authors.adoc[]
include::shared/mirrors.adoc[]
include::shared/releases.adoc[]
include::shared/attributes/attributes-{{% lang %}}.adoc[]
include::shared/{{% lang %}}/teams.adoc[]
include::shared/{{% lang %}}/mailing-lists.adoc[]
include::shared/{{% lang %}}/urls.adoc[]
toc::[]
endif::[]
ifdef::backend-pdf,backend-epub3[]
include::../../../../../shared/asciidoctor.adoc[]
endif::[]
endif::[]

ifndef::env-beastie[]
toc::[]
include::../../../../../shared/asciidoctor.adoc[]
endif::[]

本章将讨论FreeBSD为了给PCI总线上的设备编写驱动程序而提供的机制。

[[pci-probe]]
== 探测与连接

这儿的信息是关于PCI总线代码如何迭代通过未连接的设备，并查看新 加载的kld是否会连接其中一个。

=== 示例驱动程序源代码([.filename]#mypci.c#)

[.programlisting]
....
/*
 * 与PCI函数进行交互的简单KLD
 *
 * Murray Stokely
 */

#include sys/param.h		/* kernel.h中使用的定义 */
#include sys/module.h
#include sys/systm.h
#include sys/errno.h
#include sys/kernel.h		/* 模块初始化中使用的类型 */
#include sys/conf.h		/* cdevsw结构 */
#include sys/uio.h		/* uio结构 */
#include sys/malloc.h
#include sys/bus.h		/* pci总线用到的结构、原型 */

#include machine/bus.h
#include sys/rman.h
#include machine/resource.h

#include dev/pci/pcivar.h	/* 为了使用get_pci宏! */
#include dev/pci/pcireg.h

/* softc保存我们每个实例的数据。 */
struct mypci_softc {
	device_t	my_dev;
	struct cdev	*my_cdev;
};

/* 函数原型 */
static d_open_t		mypci_open;
static d_close_t	mypci_close;
static d_read_t		mypci_read;
static d_write_t	mypci_write;

/* 字符设备入口点 */

static struct cdevsw mypci_cdevsw = {
	.d_version =	D_VERSION,
	.d_open =	mypci_open,
	.d_close =	mypci_close,
	.d_read =	mypci_read,
	.d_write =	mypci_write,
	.d_name =	"mypci",
};

/*
 * 在cdevsw例程中，我们通过结构体cdev中的成员si_drv1找出我们的softc。
 * 当我们建立/dev项时，在我们的已附着的例程中，
 * 我们设置这个变量指向我们的softc。
 */

int
mypci_open(struct cdev *dev, int oflags, int devtype, struct thread *td)
{
	struct mypci_softc *sc;

	/* Look up our softc. */
	sc = dev-si_drv1;
	device_printf(sc-my_dev, "Opened successfully.\n");
	return (0);
}

int
mypci_close(struct cdev *dev, int fflag, int devtype, struct thread *td)
{
	struct mypci_softc *sc;

	/* Look up our softc. */
	sc = dev-si_drv1;
	device_printf(sc-my_dev, "Closed.\n");
	return (0);
}

int
mypci_read(struct cdev *dev, struct uio *uio, int ioflag)
{
	struct mypci_softc *sc;

	/* Look up our softc. */
	sc = dev-si_drv1;
	device_printf(sc-my_dev, "Asked to read %d bytes.\n", uio-uio_resid);
	return (0);
}

int
mypci_write(struct cdev *dev, struct uio *uio, int ioflag)
{
	struct mypci_softc *sc;

	/* Look up our softc. */
	sc = dev-si_drv1;
	device_printf(sc-my_dev, "Asked to write %d bytes.\n", uio-uio_resid);
	return (0);
}

/* PCI支持函数 */

/*
 * 将某个设置的标识与这个驱动程序支持的标识相比较。
 * 如果相符，设置描述字符并返回成功。
 */
static int
mypci_probe(device_t dev)
{

	device_printf(dev, "MyPCI Probe\nVendor ID : 0x%x\nDevice ID : 0x%x\n",
	    pci_get_vendor(dev), pci_get_device(dev));

	if (pci_get_vendor(dev) == 0x11c1) {
		printf("We've got the Winmodem, probe successful!\n");
		device_set_desc(dev, "WinModem");
		return (BUS_PROBE_DEFAULT);
	}
	return (ENXIO);
}

/* 只有当探测成功时才调用连接函数 */

static int
mypci_attach(device_t dev)
{
	struct mypci_softc *sc;

	printf("MyPCI Attach for : deviceID : 0x%x\n", pci_get_devid(dev));

	/* Look up our softc and initialize its fields. */
	sc = device_get_softc(dev);
	sc-my_dev = dev;

	/*
	 * Create a /dev entry for this device.  The kernel will assign us
	 * a major number automatically.  We use the unit number of this
	 * device as the minor number and name the character device
	 * "mypciunit".
	 */
	sc-my_cdev = make_dev(mypci_cdevsw, device_get_unit(dev),
	    UID_ROOT, GID_WHEEL, 0600, "mypci%u", device_get_unit(dev));
	sc-my_cdev-si_drv1 = sc;
	printf("Mypci device loaded.\n");
	return (0);
}

/* 分离设备。 */

static int
mypci_detach(device_t dev)
{
	struct mypci_softc *sc;

	/* Teardown the state in our softc created in our attach routine. */
	sc = device_get_softc(dev);
	destroy_dev(sc-my_cdev);
	printf("Mypci detach!\n");
	return (0);
}

/* 系统关闭期间在sync之后调用。 */

static int
mypci_shutdown(device_t dev)
{

	printf("Mypci shutdown!\n");
	return (0);
}

/*
 * 设备挂起例程。
 */
static int
mypci_suspend(device_t dev)
{

	printf("Mypci suspend!\n");
	return (0);
}

/*
 * 设备恢复（重新开始）例程。
 */
static int
mypci_resume(device_t dev)
{

	printf("Mypci resume!\n");
	return (0);
}

static device_method_t mypci_methods[] = {
	/* 设备接口 */
	DEVMETHOD(device_probe,		mypci_probe),
	DEVMETHOD(device_attach,	mypci_attach),
	DEVMETHOD(device_detach,	mypci_detach),
	DEVMETHOD(device_shutdown,	mypci_shutdown),
	DEVMETHOD(device_suspend,	mypci_suspend),
	DEVMETHOD(device_resume,	mypci_resume),

	{ 0, 0 }
};

static devclass_t mypci_devclass;

DEFINE_CLASS_0(mypci, mypci_driver, mypci_methods, sizeof(struct mypci_softc));
DRIVER_MODULE(mypci, pci, mypci_driver, mypci_devclass, 0, 0);
....

=== 示例驱动程序的[.filename]##Makefile##

[.programlisting]
....
# 驱动程序mypci的Makefile

KMOD=	mypci
SRCS=	mypci.c
SRCS+=	device_if.h bus_if.h pci_if.h

.include bsd.kmod.mk
....

如果你将上面的源文件和 [.filename]##Makefile##放入一个目录，你可以运行 ``make``编译示例驱动程序。 还有，你可以运行``make load`` 将驱动程序装载到当前正在运行的内核中，而``make unload``可在装载后卸载驱动程序。 

=== 更多资源

* http://www.pcisig.org/[PCI Special Interest Group]
* PCI System Architecture, Fourth Edition by Tom Shanley, et al.

[[pci-bus]]
== 总线资源

FreeBSD为从父总线请求资源提供了一种面向对象的机制。几乎所有设备 都是某种类型的总线（PCI, ISA, USB, SCSI等等）的孩子成员，并且这些设备 需要从他们的父总线获取资源（例如内存段, 中断线, 或者DMA通道）。

=== 基地址寄存器

为了对PCI设备做些有用的事情，你需要从PCI配置空间获取 _Base Address Registers_ (BARs)。获取BAR时的 PCI特定的细节被抽象在函数``bus_alloc_resource()``中。 

例如，一个典型的驱动程序可能在``attach()`` 函数中有些类似下面的东西：

[.programlisting]
....
    sc-bar0id = PCIR_BAR(0);
    sc-bar0res = bus_alloc_resource(dev, SYS_RES_MEMORY, sc-bar0id,
				  0, ~0, 1, RF_ACTIVE);
    if (sc-bar0res == NULL) {
        printf("Memory allocation of PCI base register 0 failed!\n");
        error = ENXIO;
        goto fail1;
    }

    sc-bar1id = PCIR_BAR(1);
    sc-bar1res = bus_alloc_resource(dev, SYS_RES_MEMORY, sc-bar1id,
				  0, ~0, 1, RF_ACTIVE);
    if (sc-bar1res == NULL) {
        printf("Memory allocation of PCI base register 1 failed!\n");
        error =  ENXIO;
        goto fail2;
    }
    sc-bar0_bt = rman_get_bustag(sc-bar0res);
    sc-bar0_bh = rman_get_bushandle(sc-bar0res);
    sc-bar1_bt = rman_get_bustag(sc-bar1res);
    sc-bar1_bh = rman_get_bushandle(sc-bar1res);
....

每个基址寄存器的句柄被保存在``softc`` 结构中，以便以后可以使用它们向设备写入。

然后就能使用这些句柄与``bus_space_*``函数一起 读写设备寄存器。例如，驱动程序可能包含如下的快捷函数，用来读取板子 特定的寄存器：

[.programlisting]
....
uint16_t
board_read(struct ni_softc *sc, uint16_t address)
{
    return bus_space_read_2(sc-bar1_bt, sc-bar1_bh, address);
}
....

类似的，可以用下面的函数写寄存器：

[.programlisting]
....
void
board_write(struct ni_softc *sc, uint16_t address, uint16_t value)
{
    bus_space_write_2(sc-bar1_bt, sc-bar1_bh, address, value);
}
....

这些函数以8位，16位和32位的版本存在，你应当相应地使用 ``bus_space_{read|write}_{1|2|4}``。

[NOTE]
====
在 FreeBSD 7.0 和更高版本中， 可以用 `bus_*` 函数来代替 `bus_space_*`。 `bus_*` 函数使用的参数是 [type]#struct resource *# 指针， 而不是 bus tag 和句柄。 这样， 您就可以将 `softc` 中的 bus tag 和 bus 句柄这两个成员变量去掉， 并将 `board_read()` 函数改写为：

[.programlisting]
....
uint16_t
board_read(struct ni_softc *sc, uint16_t address)
{
	return (bus_read(sc-bar1res, address));
}
....

====

=== 中断

中断按照和分配内存资源相似的方式从面向对象的总线代码分配。首先， 必须从父总线分配IRQ资源，然后必须设置中断处理函数来处理这个IRQ。 

再一次，来自设备``attach()``函数的例子比文字 更具说明性。

[.programlisting]
....
/* 取得IRQ资源 */

    sc-irqid = 0x0;
    sc-irqres = bus_alloc_resource(dev, SYS_RES_IRQ, (sc-irqid),
				  0, ~0, 1, RF_SHAREABLE | RF_ACTIVE);
    if (sc-irqres == NULL) {
	printf("IRQ allocation failed!\n");
	error = ENXIO;
	goto fail3;
    }

    /* 现在我们应当设置中断处理函数 */

    error = bus_setup_intr(dev, sc-irqres, INTR_TYPE_MISC,
			   my_handler, sc, (sc-handler));
    if (error) {
	printf("Couldn't set up irq\n");
	goto fail4;
    }
....

在设备的分离例程中必须注意一些问题。你必须停顿设备的中断流， 并移除中断处理函数。一旦``bus_teardown_intr()`` 返回，你知道你的中断处理函数不会再被调用，并且所有可能已经执行了 这个中断处理函数的线程都已经返回。由于此函数可以睡眠，调用此函数时 你必须不能拥有任何互斥体。

=== DMA

本节已废弃，只是由于历史原因而给出。处理这些问题的适当方法是 使用``bus_space_dma*()``函数。当更新这一节以反映 那样用法时，这段就可能被去掉。然而，目前API还不断有些变动，因此一旦 它们固定下来后，更新这一节来反映那些改动就很好了。

在PC上，想进行总线主控DMA的外围设备必须处理物理地址，由于 FreeBSD使用虚拟内存并且只处理虚地址，这仍是个问题。幸运的是，有个 函数，``vtophys()``可以帮助我们。

[.programlisting]
....
#include vm/vm.h
#include vm/pmap.h

#define vtophys(virtual_address) (...)
....

然而这个解决办法在alpha上有点不一样，并且我们真正想要的是一个 称为``vtobus()``的函数。

[.programlisting]
....
#if defined(__alpha__)
#define vtobus(va)      alpha_XXX_dmamap((vm_offset_t)va)
#else
#define vtobus(va)      vtophys(va)
#endif
....

=== 取消分配资源

取消``attach()``期间分配的所有资源非常重要。 必须小心谨慎，即使在失败的条件下也要保证取消分配那些正确的东西， 这样当你的驱动程序去掉后系统仍然可以使用。
