I'm very pleased to announce the release of our new website and documentation using the new toolchain with Hugo and AsciiDoctor. To get more information about the new toolchain please read the FreeBSD Documentation Project Primer[1], Hugo docs[2] and AsciiDoctor docs[3]. Acknowledgment: Benedict Reuschling <bcr@> Glen Barber <gjb@> Hiroki Sato <hrs@> Li-Wen Hsu <lwhsu@> Sean Chittenden <seanc@> The FreeBSD Foundation [1] https://docs.FreeBSD.org/en/books/fdp-primer/ [2] https://gohugo.io/documentation/ [3] https://docs.asciidoctor.org/home/ Approved by: doceng, core
405 lines
11 KiB
Text
405 lines
11 KiB
Text
---
|
|
title: Chapter 11. PCI Devices
|
|
prev: books/arch-handbook/isa
|
|
next: books/arch-handbook/scsi
|
|
---
|
|
|
|
[[pci]]
|
|
= PCI Devices
|
|
:doctype: book
|
|
:toc: macro
|
|
:toclevels: 1
|
|
:icons: font
|
|
:sectnums:
|
|
:sectnumlevels: 6
|
|
:source-highlighter: rouge
|
|
:experimental:
|
|
:skip-front-matter:
|
|
:xrefstyle: basic
|
|
:relfileprefix: ../
|
|
:outfilesuffix:
|
|
:sectnumoffset: 11
|
|
|
|
include::shared/mirrors.adoc[]
|
|
include::shared/authors.adoc[]
|
|
include::shared/releases.adoc[]
|
|
include::shared/en/mailing-lists.adoc[]
|
|
include::shared/en/teams.adoc[]
|
|
include::shared/en/urls.adoc[]
|
|
|
|
toc::[]
|
|
|
|
This chapter will talk about the FreeBSD mechanisms for writing a device driver for a device on a PCI bus.
|
|
|
|
[[pci-probe]]
|
|
== Probe and Attach
|
|
|
|
Information here about how the PCI bus code iterates through the unattached devices and see if a newly loaded kld will attach to any of them.
|
|
|
|
=== Sample Driver Source ([.filename]#mypci.c#)
|
|
|
|
[.programlisting]
|
|
....
|
|
/*
|
|
* 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);
|
|
....
|
|
|
|
=== [.filename]#Makefile# for Sample Driver
|
|
|
|
[.programlisting]
|
|
....
|
|
# Makefile for mypci driver
|
|
|
|
KMOD= mypci
|
|
SRCS= mypci.c
|
|
SRCS+= device_if.h bus_if.h pci_if.h
|
|
|
|
.include <bsd.kmod.mk>
|
|
....
|
|
|
|
If you place the above source file and [.filename]#Makefile# into a directory, you may run `make` to compile the sample driver. Additionally, you may run `make load` to load the driver into the currently running kernel and `make unload` to unload the driver after it is loaded.
|
|
|
|
=== Additional Resources
|
|
|
|
* http://www.pcisig.org/[PCI Special Interest Group]
|
|
* PCI System Architecture, Fourth Edition by Tom Shanley, et al.
|
|
|
|
[[pci-bus]]
|
|
== Bus Resources
|
|
|
|
FreeBSD provides an object-oriented mechanism for requesting resources from a parent bus. Almost all devices will be a child member of some sort of bus (PCI, ISA, USB, SCSI, etc) and these devices need to acquire resources from their parent bus (such as memory segments, interrupt lines, or DMA channels).
|
|
|
|
=== Base Address Registers
|
|
|
|
To do anything particularly useful with a PCI device you will need to obtain the _Base Address Registers_ (BARs) from the PCI Configuration space. The PCI-specific details of obtaining the BAR are abstracted in the `bus_alloc_resource()` function.
|
|
|
|
For example, a typical driver might have something similar to this in the `attach()` function:
|
|
|
|
[.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);
|
|
....
|
|
|
|
Handles for each base address register are kept in the `softc` structure so that they can be used to write to the device later.
|
|
|
|
These handles can then be used to read or write from the device registers with the `bus_space_*` functions. For example, a driver might contain a shorthand function to read from a board specific register like this:
|
|
|
|
[.programlisting]
|
|
....
|
|
uint16_t
|
|
board_read(struct ni_softc *sc, uint16_t address)
|
|
{
|
|
return bus_space_read_2(sc->bar1_bt, sc->bar1_bh, address);
|
|
}
|
|
....
|
|
|
|
Similarly, one could write to the registers with:
|
|
|
|
[.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);
|
|
}
|
|
....
|
|
|
|
These functions exist in 8bit, 16bit, and 32bit versions and you should use `bus_space_{read|write}_{1|2|4}` accordingly.
|
|
|
|
[NOTE]
|
|
====
|
|
In FreeBSD 7.0 and later, you can use the `bus_*` functions instead of `bus_space_*`. The `bus_*` functions take a struct resource * pointer instead of a bus tag and handle. Thus, you could drop the bus tag and bus handle members from the `softc` and rewrite the `board_read()` function as:
|
|
|
|
[.programlisting]
|
|
....
|
|
uint16_t
|
|
board_read(struct ni_softc *sc, uint16_t address)
|
|
{
|
|
return (bus_read(sc->bar1res, address));
|
|
}
|
|
....
|
|
|
|
====
|
|
|
|
=== Interrupts
|
|
|
|
Interrupts are allocated from the object-oriented bus code in a way similar to the memory resources. First an IRQ resource must be allocated from the parent bus, and then the interrupt handler must be set up to deal with this IRQ.
|
|
|
|
Again, a sample from a device `attach()` function says more than words.
|
|
|
|
[.programlisting]
|
|
....
|
|
/* 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;
|
|
}
|
|
....
|
|
|
|
Some care must be taken in the detach routine of the driver. You must quiesce the device's interrupt stream, and remove the interrupt handler. Once `bus_teardown_intr()` has returned, you know that your interrupt handler will no longer be called and that all threads that might have been executing this interrupt handler have returned. Since this function can sleep, you must not hold any mutexes when calling this function.
|
|
|
|
=== DMA
|
|
|
|
This section is obsolete, and present only for historical reasons. The proper methods for dealing with these issues is to use the `bus_space_dma*()` functions instead. This paragraph can be removed when this section is updated to reflect that usage. However, at the moment, the API is in a bit of flux, so once that settles down, it would be good to update this section to reflect that.
|
|
|
|
On the PC, peripherals that want to do bus-mastering DMA must deal with physical addresses. This is a problem since FreeBSD uses virtual memory and deals almost exclusively with virtual addresses. Fortunately, there is a function, `vtophys()` to help.
|
|
|
|
[.programlisting]
|
|
....
|
|
#include <vm/vm.h>
|
|
#include <vm/pmap.h>
|
|
|
|
#define vtophys(virtual_address) (...)
|
|
....
|
|
|
|
The solution is a bit different on the alpha however, and what we really want is a function called `vtobus()`.
|
|
|
|
[.programlisting]
|
|
....
|
|
#if defined(__alpha__)
|
|
#define vtobus(va) alpha_XXX_dmamap((vm_offset_t)va)
|
|
#else
|
|
#define vtobus(va) vtophys(va)
|
|
#endif
|
|
....
|
|
|
|
=== Deallocating Resources
|
|
|
|
It is very important to deallocate all of the resources that were allocated during `attach()`. Care must be taken to deallocate the correct stuff even on a failure condition so that the system will remain usable while your driver dies.
|