Implementing an Interrupt Handler

Example 8-1 is a contrived Newbus driver designed to demonstrate interrupt handlers. Example 8-1 sets up an interrupt handler on the parallel port; on read, it sleeps until it receives an interrupt.

Note

Take a quick look at this code and try to discern some of its structure. If you don’t understand all of it, don’t worry; an explanation follows.

Example 8-1. pint.c

#include <sys/param.h>
#include <sys/module.h>
#include <sys/kernel.h>
#include <sys/systm.h>

#include <sys/conf.h>
#include <sys/uio.h>
#include <sys/bus.h>
#include <sys/malloc.h>

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

#include <dev/ppbus/ppbconf.h>
#include "ppbus_if.h"
#include <dev/ppbus/ppbio.h>

#define PINT_NAME               "pint"
#define BUFFER_SIZE             256

struct pint_data {
        int                     sc_irq_rid;
        struct resource        *sc_irq_resource;
        void                   *sc_irq_cookie;
        device_t                sc_device;
        struct cdev            *sc_cdev;
        short                   sc_state;
#define PINT_OPEN               0x01
        char                   *sc_buffer;
        int                     sc_length;
};

static d_open_t                 pint_open;
static d_close_t                pint_close;
static d_read_t                 pint_read;
static d_write_t                pint_write;

static struct cdevsw pint_cdevsw = {
        .d_version =            D_VERSION,
        .d_open =               pint_open,
        .d_close =              pint_close,
        .d_read =               pint_read,
        .d_write =              pint_write,
        .d_name =               PINT_NAME
};

static devclass_t pint_devclass;

static int
pint_open(struct cdev *dev, int oflags, int devtype, struct thread *td)
{
        struct pint_data *sc = dev->si_drv1;
        device_t pint_device = sc->sc_device;
        device_t ppbus = device_get_parent(pint_device);
        int error;

        ppb_lock(ppbus);

        if (sc->sc_state) {
                ppb_unlock(ppbus);
                return (EBUSY);
        } else
                sc->sc_state |= PINT_OPEN;

        error = ppb_request_bus(ppbus, pint_device, PPB_WAIT | PPB_INTR);
        if (error) {
                sc->sc_state = 0;
                ppb_unlock(ppbus);
                return (error);
        }

        ppb_wctr(ppbus, 0);
        ppb_wctr(ppbus, IRQENABLE);

        ppb_unlock(ppbus);
        return (0);
}

static int
pint_close(struct cdev *dev, int fflag, int devtype, struct thread *td)
{
        struct pint_data *sc = dev->si_drv1;
        device_t pint_device = sc->sc_device;
        device_t ppbus = device_get_parent(pint_device);

        ppb_lock(ppbus);

        ppb_wctr(ppbus, 0);
        ppb_release_bus(ppbus, pint_device);
        sc->sc_state = 0;

        ppb_unlock(ppbus);
        return (0);
}

static int
pint_write(struct cdev *dev, struct uio *uio, int ioflag)
{
        struct pint_data *sc = dev->si_drv1;
        device_t pint_device = sc->sc_device;
        int amount, error = 0;

        amount = MIN(uio->uio_resid,
            (BUFFER_SIZE - 1 - uio->uio_offset > 0) ?
             BUFFER_SIZE - 1 - uio->uio_offset : 0);
        if (amount == 0)
                return (error);

        error = uiomove(sc->sc_buffer, amount, uio);
        if (error) {
                device_printf(pint_device, "write failed
");
                return (error);
        }

        sc->sc_buffer[amount] = '';
        sc->sc_length = amount;

        return (error);
}

static int
pint_read(struct cdev *dev, struct uio *uio, int ioflag)
{
        struct pint_data *sc = dev->si_drv1;
        device_t pint_device = sc->sc_device;
        device_t ppbus = device_get_parent(pint_device);
        int amount, error = 0;

        ppb_lock(ppbus);
        error = ppb_sleep(ppbus, pint_device, PPBPRI | PCATCH, PINT_NAME, 0);
        ppb_unlock(ppbus);
        if (error)
                return (error);

        amount = MIN(uio->uio_resid,
            (sc->sc_length - uio->uio_offset > 0) ?
             sc->sc_length - uio->uio_offset : 0);

        error = uiomove(sc->sc_buffer + uio->uio_offset, amount, uio);
        if (error)
                device_printf(pint_device, "read failed
");

        return (error);
}

static void
pint_intr(void *arg)
{
        struct pint_data *sc = arg;
        device_t pint_device = sc->sc_device;

#ifdef INVARIANTS
        device_t ppbus = device_get_parent(pint_device);
        ppb_assert_locked(ppbus);
#endif

        wakeup(pint_device);
}

static void
pint_identify(driver_t *driver, device_t parent)
{
        device_t dev;

        dev = device_find_child(parent, PINT_NAME, −1);
        if (!dev)
                BUS_ADD_CHILD(parent, 0, PINT_NAME, −1);
}

static int
pint_probe(device_t dev)
{
        /* probe() is always OK. */
        device_set_desc(dev, "Interrupt Handler Example");

        return (BUS_PROBE_SPECIFIC);
}

static int
pint_attach(device_t dev)
{
        struct pint_data *sc = device_get_softc(dev);
        int error, unit = device_get_unit(dev);

        /* Declare our interrupt handler. */
        sc->sc_irq_rid = 0;
        sc->sc_irq_resource = bus_alloc_resource_any(dev, SYS_RES_IRQ,
            &sc->sc_irq_rid, RF_ACTIVE | RF_SHAREABLE);

        /* Interrupts are mandatory. */
        if (!sc->sc_irq_resource) {
                device_printf(dev,
                    "unable to allocate interrupt resource
");
                return (ENXIO);
        }

        /* Register our interrupt handler. */
        error = bus_setup_intr(dev, sc->sc_irq_resource,
            INTR_TYPE_TTY | INTR_MPSAFE, NULL, pint_intr,
            sc, &sc->sc_irq_cookie);
        if (error) {
                bus_release_resource(dev, SYS_RES_IRQ, sc->sc_irq_rid,
                    sc->sc_irq_resource);
                device_printf(dev, "unable to register interrupt handler
");
                return (error);
        }

        sc->sc_buffer = malloc(BUFFER_SIZE, M_DEVBUF, M_WAITOK);

        sc->sc_device = dev;
        sc->sc_cdev = make_dev(&pint_cdevsw, unit, UID_ROOT, GID_WHEEL, 0600,
            PINT_NAME "%d", unit);
        sc->sc_cdev->si_drv1 = sc;

        return (0);
}

static int
pint_detach(device_t dev)
{
        struct pint_data *sc = device_get_softc(dev);

        destroy_dev(sc->sc_cdev);

        bus_teardown_intr(dev, sc->sc_irq_resource, sc->sc_irq_cookie);
        bus_release_resource(dev, SYS_RES_IRQ, sc->sc_irq_rid,
            sc->sc_irq_resource);

        free(sc->sc_buffer, M_DEVBUF);

        return (0);
}

static device_method_t pint_methods[] = {
        /* Device interface. */
        DEVMETHOD(device_identify,      pint_identify),
        DEVMETHOD(device_probe,         pint_probe),
        DEVMETHOD(device_attach,        pint_attach),
        DEVMETHOD(device_detach,        pint_detach),
        { 0, 0 }
};

static driver_t pint_driver = {
        PINT_NAME,
        pint_methods,
        sizeof(struct pint_data)
};

DRIVER_MODULE(pint, ppbus, pint_driver, pint_devclass, 0, 0);
MODULE_DEPEND(pint, ppbus, 1, 1, 1);

To make things easier to understand, I’ll describe the functions in Example 8-1 in the order they were written, instead of in the order they appear. To that end, I’ll begin with the pint_identify function.

pint_identify Function

The pint_identify function is the device_identify implementation for this driver. Logically, this function is required because the parallel port cannot identify its children unaided.

Here is the function definition for pint_identify (again):

static void
pint_identify(driver_t *driver, device_t parent)
{
        device_t dev;

        dev = device_find_child(parent, PINT_NAME, −1);
        if (!dev)
                BUS_ADD_CHILD(parent, 0, PINT_NAME, −1);
}

This function first determines whether the parallel port has (ever) identified a child device named PINT_NAME. If it has not, then pint_identify adds PINT_NAME to the parallel port’s list of identified children.

pint_probe Function

The pint_probe function is the device_probe implementation for this driver. Here is its function definition (again):

static int
pint_probe(device_t dev)
{
        /* probe() is always OK. */
        device_set_desc(dev, "Interrupt Handler Example");

      return (BUS_PROBE_SPECIFIC);
}

As you can see, this function always returns the success code BUS_PROBE_SPECIFIC, so Example 8-1 attaches to every device it probes. This may seem erroneous, but it is the correct behavior, as devices identified by a device_identify routine, using BUS_ADD_CHILD, are probed only by drivers with the same name. In this case, the identified device and driver name is PINT_NAME.

pint_attach Function

The pint_attach function is the device_attach implementation for this driver. Here is its function definition (again):

static int
pint_attach(device_t dev)
{
        struct pint_data *sc = device_get_softc(dev);
        int error, unit = device_get_unit(dev);

        /* Declare our interrupt handler. */
        sc->sc_irq_rid = 0;
        sc->sc_irq_resource = bus_alloc_resource_any(dev, SYS_RES_IRQ,
            &sc->sc_irq_rid, RF_ACTIVE | RF_SHAREABLE);

        /* Interrupts are mandatory. */
        if (!sc->sc_irq_resource) {
                device_printf(dev,
                    "unable to allocate interrupt resource
");
              return (ENXIO);
        }

        /* Register our interrupt handler. */
        error = bus_setup_intr(dev, sc->sc_irq_resource,
            INTR_TYPE_TTY | INTR_MPSAFE, NULL, pint_intr,
            sc, &sc->sc_irq_cookie);
        if (error) {
                bus_release_resource(dev, SYS_RES_IRQ, sc->sc_irq_rid,
                    sc->sc_irq_resource);
                device_printf(dev, "unable to register interrupt handler
");
                return (error);
        }

        sc->sc_buffer = malloc(BUFFER_SIZE, M_DEVBUF, M_WAITOK);

      sc->sc_device = dev;
        sc->sc_cdev = make_dev(&pint_cdevsw, unit, UID_ROOT, GID_WHEEL,
            0600, PINT_NAME "%d", unit);
      sc->sc_cdev->si_drv1 = sc;

        return (0);
}

This function first allocates an IRQ. If unsuccessful, the error code ENXIO (which stands for error: device not configured) is returned. Next, the pint_intr function is set up as the interrupt handler for dev (in this case, the interrupt handler is just an ithread routine). Afterward, a buffer of BUFFER_SIZE bytes is allocated. Then sc->sc_device is set to dev, Example 8-1’s character device node is created, and a pointer to the software context (sc) is saved in sc->sc_cdev->si_drv1.

pint_detach Function

The pint_detach function is the device_detach implementation for this driver. Here is its function definition (again):

static int
pint_detach(device_t dev)
{
        struct pint_data *sc = device_get_softc(dev);

      destroy_dev(sc->sc_cdev);

      bus_teardown_intr(dev, sc->sc_irq_resource, sc->sc_irq_cookie);
      bus_release_resource(dev, SYS_RES_IRQ, sc->sc_irq_rid,
            sc->sc_irq_resource);

      free(sc->sc_buffer, M_DEVBUF);

        return (0);
}

This function starts by destroying Example 8-1’s device node. Once this is done, it tears down dev’s interrupt handler, releases dev’s IRQ, and frees the allocated memory.

pint_open Function

The pint_open function is defined in pint_cdevsw (that is, Example 8-1’s character device switch table) as the d_open operation. Recall that d_open operations prepare the device for I/O.

Here is the function definition for pint_open (again):

static int
pint_open(struct cdev *dev, int oflags, int devtype, struct thread *td)
{
        struct pint_data *sc = dev->si_drv1;
        device_t pint_device = sc->sc_device;
        device_t ppbus = device_get_parent(pint_device);
        int error;

      ppb_lock(ppbus);

      if (sc->sc_state) {
                ppb_unlock(ppbus);
              return (EBUSY);
        } else
              sc->sc_state |= PINT_OPEN;

        error = ppb_request_bus(ppbus, pint_device, PPB_WAIT | PPB_INTR);
        if (error) {
                sc->sc_state = 0;
                ppb_unlock(ppbus);
                return (error);
        }

      ppb_wctr(ppbus, 0);
      ppb_wctr(ppbus, IRQENABLE);

        ppb_unlock(ppbus);
        return (0);
}

This function first acquires the parallel port mutex. Then the value of sc->sc_state is examined. If it does not equal 0, which indicates that another process has opened the device, the error code EBUSY is returned; otherwise, pint_open “opens” the device. Opening the device, in this case, means setting sc->sc_state to PINT_OPEN. Afterward, the ppb_request_bus function is called to mark pint_device as the owner of the parallel port. Naturally, pint_device is our device (that is, it points to dev from pint_attach).

Note

Owning the parallel port lets a device transfer data to and from it.

Finally, before enabling interrupts, pint_open clears the parallel port’s control register.

pint_close Function

The pint_close function is defined in pint_cdevsw as the d_close operation. Here is its function definition (again):

static int
pint_close(struct cdev *dev, int fflag, int devtype, struct thread *td)
{
        struct pint_data *sc = dev->si_drv1;
        device_t pint_device = sc->sc_device;
        device_t ppbus = device_get_parent(pint_device);

      ppb_lock(ppbus);

      ppb_wctr(ppbus, 0);
      ppb_release_bus(ppbus, pint_device);
      sc->sc_state = 0;

        ppb_unlock(ppbus);
        return (0);
}

This function first acquires the parallel port mutex. Then interrupts on the parallel port are disabled (for all intents and purposes, clearing the control register, which is what the above code does, disables interrupts). Next, the ppb_release_bus function is called to relinquish ownership of the parallel port. Finally, sc->sc_state is zeroed, so that another process can open this device.

pint_write Function

The pint_write function is defined in pint_cdevsw as the d_write operation. This function acquires a character string from user space and stores it.

Here is the function definition for pint_write (again):

static int
pint_write(struct cdev *dev, struct uio *uio, int ioflag)
{
        struct pint_data *sc = dev->si_drv1;
        device_t pint_device = sc->sc_device;
        int amount, error = 0;

        amount = MIN(uio->uio_resid,
            (BUFFER_SIZE - 1 - uio->uio_offset > 0) ?
             BUFFER_SIZE - 1 - uio->uio_offset : 0);
        if (amount == 0)
                return (error);

        error = uiomove(sc->sc_buffer, amount, uio);
        if (error) {
                device_printf(pint_device, "write failed
");
                return (error);
        }

        sc->sc_buffer[amount] = '';
        sc->sc_length = amount;

        return (error);
}

This function is fundamentally identical to the echo_write function described in echo_write Function. Consequently, I won’t walk through it again here.

pint_read Function

The pint_read function is defined in pint_cdevsw as the d_read operation. This function sleeps on entry. It also returns the stored character string to user space.

Here is the function definition for pint_read (again):

static int
pint_read(struct cdev *dev, struct uio *uio, int ioflag)
{
        struct pint_data *sc = dev->si_drv1;
        device_t pint_device = sc->sc_device;
        device_t ppbus = device_get_parent(pint_device);
        int amount, error = 0;

      ppb_lock(ppbus);
        error = ppb_sleep(ppbus, pint_device, PPBPRI | PCATCH,
            PINT_NAME, 0);
        ppb_unlock(ppbus);
        if (error)
                return (error);

        amount = MIN(uio->uio_resid,
            (sc->sc_length - uio->uio_offset > 0) ?
             sc->sc_length - uio->uio_offset : 0);

        error = uiomove(sc->sc_buffer + uio->uio_offset, amount, uio);
        if (error)
                device_printf(pint_device, "read failed
");

        return (error);
}

This function begins by acquiring the parallel port mutex. Then it sleeps on the channel pint_device.

Note

The ppb_sleep function releases the parallel port mutex before sleeping. Of course, it also reacquires the parallel port mutex before returning to its caller.

The remnants of this function are basically identical to the echo_read function described in echo_read Function, so we won’t discuss them again here.

pint_intr Function

The pint_intr function is the interrupt handler for Example 8-1. Here is its function definition (again):

static void
pint_intr(void *arg)
{
        struct pint_data *sc = arg;
        device_t pint_device = sc->sc_device;

#ifdef INVARIANTS
        device_t ppbus = device_get_parent(pint_device);
        ppb_assert_locked(ppbus);
#endif

      wakeup(pint_device);
}

As you can see, this function simply wakes up every thread sleeping on pint_device.

Note

Parallel port interrupt handlers are unique, because they get invoked with the parallel port mutex already held. Conversely, normal interrupt handlers need to explicitly acquire their own locks.

Don’t Panic

Now that we’ve walked through Example 8-1, let’s give it a try:

$ sudo kldload ./pint.ko
$ su
Password:
# echo "DON'T PANIC" > /dev/pint0
# cat /dev/pint0 &
[1] 954
# ps | head -n 1 && ps | grep "cat"
  PID  TT  STAT      TIME COMMAND
  954  v1  I      0:00.03 cat /dev/pint0

Apparently it works. But how do we generate an interrupt to test our interrupt handler?

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.14.131.47