第 11 章。PCI 设备

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

11.1。探测和连接

这里的信息介绍了 PCI 总线代码如何遍历未连接的设备,并查看新加载的 KLD 是否会连接到其中的任何一个。

11.1.1。示例驱动程序源代码(mypci.c

/*
 * Simple KLD to play with the PCI functions.
 *
 * Murray Stokely
 */

#include <sys/param.h>		/* defines used in kernel.h */
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/errno.h>
#include <sys/kernel.h>		/* types used in module initialization */
#include <sys/conf.h>		/* cdevsw struct */
#include <sys/uio.h>		/* uio struct */
#include <sys/malloc.h>
#include <sys/bus.h>		/* structs, prototypes for pci bus stuff and DEVMETHOD macros! */

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

#include <dev/pci/pcivar.h>	/* For pci_get macros! */
#include <dev/pci/pcireg.h>

/* The softc holds our per-instance data. */
struct mypci_softc {
	device_t	my_dev;
	struct cdev	*my_cdev;
};

/* Function prototypes */
static d_open_t		mypci_open;
static d_close_t	mypci_close;
static d_read_t		mypci_read;
static d_write_t	mypci_write;

/* Character device entry points */

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",
};

/*
 * In the cdevsw routines, we find our softc by using the si_drv1 member
 * of struct cdev.  We set this variable to point to our softc in our
 * attach routine when we create the /dev entry.
 */

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 %zd 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 %zd bytes.\n", uio->uio_resid);
	return (0);
}

/* PCI Support Functions */

/*
 * Compare the device ID of this device against the IDs that this driver
 * supports.  If there is a match, set the description and return success.
 */
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);
}

/* Attach function is only called if the probe is successful. */

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
	 * "mypci<unit>".
	 */
	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);
}

/* Detach device. */

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);
}

/* Called during system shutdown after sync. */

static int
mypci_shutdown(device_t dev)
{

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

/*
 * Device suspend routine.
 */
static int
mypci_suspend(device_t dev)
{

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

/*
 * Device resume routine.
 */
static int
mypci_resume(device_t dev)
{

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

static device_method_t mypci_methods[] = {
	/* Device interface */
	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),

	DEVMETHOD_END
};

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);

11.1.2。示例驱动程序的 Makefile

# Makefile for mypci driver

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

.include <bsd.kmod.mk>

如果您将上面的源文件和 Makefile 放入一个目录中,您可以运行 make 来编译示例驱动程序。此外,您可以运行 make load 将驱动程序加载到当前运行的内核中,以及运行 make unload 在驱动程序加载后卸载它。

11.1.3。其他资源

11.2。总线资源

FreeBSD 提供了一种面向对象的机制,用于从父总线请求资源。几乎所有设备都是某种总线的子成员(PCI、ISA、USB、SCSI 等),这些设备需要从其父总线获取资源(如内存段、中断线或 DMA 频道)。

11.2.1。基地址寄存器

为了对 PCI 设备进行任何特别有用的操作,您需要从 PCI 配置空间获取基地址寄存器(BAR)。获取 BAR 的 PCI 特定细节在 bus_alloc_resource() 函数中进行了抽象。

例如,一个典型的驱动程序在 attach() 函数中可能包含类似于以下内容的内容

    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_* 函数读取或写入设备寄存器。例如,驱动程序可能包含一个用于从板卡特定寄存器读取的简写函数,如下所示

uint16_t
board_read(struct ni_softc *sc, uint16_t address)
{
    return bus_space_read_2(sc->bar1_bt, sc->bar1_bh, address);
}

类似地,可以使用以下方法写入寄存器

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}

在 FreeBSD 7.0 及更高版本中,您可以使用 bus_* 函数代替 bus_space_*bus_* 函数使用 struct resource * 指针而不是总线标记和句柄。因此,您可以从 softc 中删除总线标记和总线句柄成员,并将 board_read() 函数重写为

uint16_t
board_read(struct ni_softc *sc, uint16_t address)
{
	return (bus_read(sc->bar1res, address));
}

11.2.2。中断

中断从面向对象的总线代码中分配,方式类似于内存资源。首先,必须从父总线分配一个 IRQ 资源,然后必须设置中断处理程序来处理此 IRQ。

同样,来自设备 attach() 函数的示例比文字更能说明问题。

/* Get the IRQ resource */

    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;
    }

    /* Now we should set up the interrupt handler */

    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;
    }

在驱动程序的 detach 例程中必须小心。您必须使设备的中断流静止,并删除中断处理程序。bus_teardown_intr() 返回后,您就知道您的中断处理程序将不再被调用,并且所有可能正在执行此中断处理程序的线程都已返回。由于此函数可能会休眠,因此在调用此函数时您不能持有任何互斥锁。

11.2.3。DMA

本节已过时,仅出于历史原因保留。处理这些问题的正确方法是使用 bus_space_dma*() 函数。当本节更新以反映该用法时,可以删除本段。但是,目前,API 处于一些变化中,因此一旦稳定下来,最好更新本节以反映这一点。

在 PC 上,想要执行总线主控 DMA 的外设必须处理物理地址。这是一个问题,因为 FreeBSD 使用虚拟内存,几乎完全处理虚拟地址。幸运的是,有一个函数 vtophys() 可以帮助。

#include <vm/vm.h>
#include <vm/pmap.h>

#define vtophys(virtual_address) (...)

然而,在 alpha 上,解决方案略有不同,我们真正想要的是一个名为 vtobus() 的函数。

#if defined(__alpha__)
#define vtobus(va)      alpha_XXX_dmamap((vm_offset_t)va)
#else
#define vtobus(va)      vtophys(va)
#endif

11.2.4。释放资源

释放 attach() 期间分配的所有资源非常重要。即使在失败的情况下也必须小心释放正确的内容,以便系统在您的驱动程序崩溃时仍然可用。


最后修改时间:2024 年 3 月 9 日,作者 Danilo G. Baio