Chapter 9. Case Study: Parallel Port Printer Driver

image with no caption

This chapter is the second case study in this book. In this chapter, we’ll go through lpt(4), the parallel port printer driver. lpt(4), by default, is configured to be interrupt-driven, which gives us an opportunity to go through a nontrivial interrupt handler. Aside from this, I chose to profile lpt(4) because it uses almost every topic described in the previous chapters. It’s also relatively short.

Note

To improve readability, some of the variables and functions presented in this chapter have been renamed and restructured from their counterparts in the FreeBSD source.

Code Analysis

Example 9-1 provides a terse, source-level overview of lpt(4).

Example 9-1. lpt.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 <sys/syslog.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>
#include <dev/ppbus/ppb_1284.h>

#include <dev/ppbus/lpt.h>
#include <dev/ppbus/lptio.h>

#define LPT_NAME        "lpt"           /* official driver name.        */
#define LPT_INIT_READY  4               /* wait up to 4 seconds.        */
#define LPT_PRI         (PZERO + 8)     /* priority.                    */
#define BUF_SIZE        1024            /* sc_buf size.                 */
#define BUF_STAT_SIZE   32              /* sc_buf_stat size.            */

struct lpt_data {
        short                   sc_state;
        char                    sc_primed;
        struct callout          sc_callout;
        u_char                  sc_ticks;
        int                     sc_irq_rid;
        struct resource        *sc_irq_resource;
        void                   *sc_irq_cookie;
        u_short                 sc_irq_status;
        void                   *sc_buf;
        void                   *sc_buf_stat;
        char                   *sc_cp;
        device_t                sc_dev;
        struct cdev            *sc_cdev;
        struct cdev            *sc_cdev_bypass;
        char                    sc_flags;
        u_char                  sc_control;
        short                   sc_transfer_count;
};

/* bits for sc_state. */
#define LP_OPEN         (1 << 0)        /* device is open.              */
#define LP_ERROR        (1 << 2)        /* error received from printer. */
#define LP_BUSY         (1 << 3)        /* printer is busy writing.     */
#define LP_TIMEOUT      (1 << 5)        /* timeout enabled.             */
#define LP_INIT         (1 << 6)        /* initializing in lpt_open.    */
#define LP_INTERRUPTED  (1 << 7)        /* write call was interrupted.  */
#define LP_HAVEBUS      (1 << 8)        /* driver owns the bus.         */

/* bits for sc_ticks. */
#define LP_TOUT_INIT    10              /* initial timeout: 1/10 sec.   */
#define LP_TOUT_MAX     1               /* max timeout: 1/1 sec.        */

/* bits for sc_irq_status. */
#define LP_HAS_IRQ      0x01            /* we have an IRQ available.    */
#define LP_USE_IRQ      0x02            /* our IRQ is in use.           */
#define LP_ENABLE_IRQ   0x04            /* enable our IRQ on open.      */
#define LP_ENABLE_EXT   0x10            /* enable extended mode.        */

/* bits for sc_flags. */
#define LP_NO_PRIME     0x10            /* don't prime the printer.     */
#define LP_PRIME_OPEN   0x20            /* prime on every open.         */
#define LP_AUTO_LF      0x40            /* automatic line feed.         */
#define LP_BYPASS       0x80            /* bypass printer ready checks. */

/* masks to interrogate printer status. */
#define LP_READY_MASK   (LPS_NERR | LPS_SEL | LPS_OUT | LPS_NBSY)
#define LP_READY        (LPS_NERR | LPS_SEL |           LPS_NBSY)

/* used in polling code. */
#define LPS_INVERT      (LPS_NERR | LPS_SEL |           LPS_NACK | LPS_NBSY)
#define LPS_MASK        (LPS_NERR | LPS_SEL | LPS_OUT | LPS_NACK | LPS_NBSY)
#define NOT_READY(bus)  ((ppb_rstr(bus) ^ LPS_INVERT) & LPS_MASK)
#define MAX_SPIN        20              /* wait up to 20 usec.          */
#define MAX_SLEEP       (hz * 5)        /* timeout while waiting.       */

static d_open_t                 lpt_open;
static d_close_t                lpt_close;
static d_read_t                 lpt_read;
static d_write_t                lpt_write;
static d_ioctl_t                lpt_ioctl;

static struct cdevsw lpt_cdevsw = {
        .d_version =            D_VERSION,
        .d_open =               lpt_open,
        .d_close =              lpt_close,
        .d_read =               lpt_read,
        .d_write =              lpt_write,
        .d_ioctl =              lpt_ioctl,
        .d_name =               LPT_NAME
};

static devclass_t lpt_devclass;

static void
lpt_identify(driver_t *driver, device_t parent)
{
...
}

static int
lpt_request_ppbus(device_t dev, int how)
{
...
}

static int
lpt_release_ppbus(device_t dev)
{
...
}

static int
lpt_port_test(device_t ppbus, u_char data, u_char mask)
{
...
}

static int
lpt_detect(device_t dev)
{
...
}

static int
lpt_probe(device_t dev)
{
...
}

static void
lpt_intr(void *arg)
{
...
}

static int
lpt_attach(device_t dev)
{
...
}

static int
lpt_detach(device_t dev)
{
...
}

static void
lpt_timeout(void *arg)
{
...
}

static int
lpt_open(struct cdev *dev, int oflags, int devtype, struct thread *td)
{
...
}

static int
lpt_close(struct cdev *dev, int fflag, int devtype, struct thread *td)
{
...
}

static int
lpt_read(struct cdev *dev, struct uio *uio, int ioflag)
{
...
}

static int
lpt_push_bytes(struct lpt_data *sc)
{
...
}

static int
lpt_write(struct cdev *dev, struct uio *uio, int ioflag)
{
...
}

static int
lpt_ioctl(struct cdev *dev, u_long cmd, caddr_t data, int fflag,
    struct thread *td)
{
...
}

static device_method_t lpt_methods[] = {
        DEVMETHOD(device_identify,      lpt_identify),
        DEVMETHOD(device_probe,         lpt_probe),
        DEVMETHOD(device_attach,        lpt_attach),
        DEVMETHOD(device_detach,        lpt_detach),
        { 0, 0 }
};

static driver_t lpt_driver = {
        LPT_NAME,
        lpt_methods,
        sizeof(struct lpt_data)
};

DRIVER_MODULE(lpt, ppbus, lpt_driver, lpt_devclass, 0, 0);
MODULE_DEPEND(lpt, ppbus, 1, 1, 1);

Example 9-1 is provided as a convenience; as I go through the code for lpt(4) you can refer to it to see how lpt(4)’s functions and structures are laid out.

To make things easier to follow, I’ll analyze the functions in lpt(4) in the approximate order they would execute in (rather than in the order they appear). To that end, I’ll begin with the lpt_identify function.

lpt_identify Function

The lpt_identify function is the device_identify implementation for lpt(4). Logically, this function is required because the parallel port cannot identify its children unaided.

Here is the function definition for lpt_identify:

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

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

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

lpt_probe Function

The lpt_probe function is the device_probe implementation for lpt(4). Here is its function definition:

static int
lpt_probe(device_t dev)
{
        if (!lpt_detect(dev))
                return (ENXIO);

        device_set_desc(dev, "Printer");

        return (BUS_PROBE_SPECIFIC);
}

This function simply calls lpt_detect to detect (that is, probe for) the presence of a printer.

lpt_detect Function

As mentioned in the previous section, lpt_detect detects the presence of a printer. It works by writing to the parallel port’s data register. If a printer is present, it can read back the value just written.

Here is the function definition for lpt_detect:

static int
lpt_detect(device_t dev)
{
        device_t ppbus = device_get_parent(dev);
      static u_char test[18] = {
                0x55,                   /* alternating zeros.   */
                0xaa,                   /* alternating ones.    */
                0xfe, 0xfd, 0xfb, 0xf7,
                0xef, 0xdf, 0xbf, 0x7f, /* walking zero.        */
                0x01, 0x02, 0x04, 0x08,
                0x10, 0x20, 0x40, 0x80  /* walking one.         */
        };
        int i, error, success = 1;      /* assume success.      */

      ppb_lock(ppbus);

        error = lpt_request_ppbus(dev, PPB_DONTWAIT);
        if (error) {
                ppb_unlock(ppbus);
                device_printf(dev, "cannot allocate ppbus (%d)!
", error);
                return (0);
        }

        for (i = 0; i < 18; i++)
                if (!lpt_port_test(ppbus, test[i], 0xff)) {
                        success = 0;
                        break;
                }

      ppb_wdtr(ppbus, 0);
      ppb_wctr(ppbus, 0);

      lpt_release_ppbus(dev);
      ppb_unlock(ppbus);

        return (success);
}

This function first acquires the parallel port mutex. Next, lpt(4) is assigned ownership of the parallel port. Then lpt_port_test is called to write to and read from the parallel port’s data register. The values written to this 8-bit register are housed in test[] and are designed to toggle all 8 bits.

Once this is done, the parallel port’s data and control registers are cleared, ownership of the parallel port is relinquished, and the parallel port mutex is released.

lpt_port_test Function

The lpt_port_test function is called by lpt_detect to determine whether a printer is present. Here is its function definition:

static int
lpt_port_test(device_t ppbus, u_char data, u_char mask)
{
        int temp, timeout = 10000;

        data &= mask;
      ppb_wdtr(ppbus, data);

        do {
                DELAY(10);
                temp = ppb_rdtr(ppbus) & mask;
        } while (temp != data && --timeout);

      return (temp == data);
}

This function takes an 8-bit value and writes it to the parallel port’s data register. Then it reads from that register and returns whether the value written and read match.

lpt_attach Function

The lpt_attach function is the device_attach implementation for lpt(4). Here is its function definition:

static int
lpt_attach(device_t dev)
{
        device_t ppbus = device_get_parent(dev);
        struct lpt_data *sc = device_get_softc(dev);
        int error, unit = device_get_unit(dev);

      sc->sc_primed = 0;
      ppb_init_callout(ppbus, &sc->sc_callout, 0);

        ppb_lock(ppbus);
        error = lpt_request_ppbus(dev, PPB_DONTWAIT);
        if (error) {
                ppb_unlock(ppbus);
                device_printf(dev, "cannot allocate ppbus (%d)!
", error);
                return (0);
        }

      ppb_wctr(ppbus, LPC_NINIT);

        lpt_release_ppbus(dev);
        ppb_unlock(ppbus);

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

        /* Register our interrupt handler. */
        if (sc->sc_irq_resource) {
                error = bus_setup_intr(dev, sc->sc_irq_resource,
                    INTR_TYPE_TTY | INTR_MPSAFE, NULL, lpt_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_irq_status = LP_HAS_IRQ | LP_USE_IRQ | LP_ENABLE_IRQ;
                device_printf(dev, "interrupt-driven port
");
        } else {
                sc->sc_irq_status = 0;
                device_printf(dev, "polled port
");
        }

      sc->sc_buf = malloc(BUF_SIZE, M_DEVBUF, M_WAITOK);
      sc->sc_buf_stat = malloc(BUF_STAT_SIZE, M_DEVBUF, M_WAITOK);

        sc->sc_dev = dev;

        sc->sc_cdev = make_dev(&lpt_cdevsw, unit, UID_ROOT, GID_WHEEL, 0600,
            LPT_NAME "%d", unit);
        sc->sc_cdev->si_drv1 = sc;
        sc->sc_cdev->si_drv2 = 0;

        sc->sc_cdev_bypass = make_dev(&lpt_cdevsw, unit, UID_ROOT, GID_WHEEL,
            0600, LPT_NAME "%d.ctl", unit);
        sc->sc_cdev_bypass->si_drv1 = sc;
        sc->sc_cdev_bypass->si_drv2 = (void *)LP_BYPASS;

        return (0);
}

This function can be split into five parts. The first sets sc->sc_primed to 0 to indicate that the printer needs to be primed. It also initializes lpt(4)’s callout structure. The second part essentially changes the electrical signal at pin 16, dubbed nINIT, from high to low causing the printer to initiate an internal reset.

Note

As most signals are active high, the n in nINIT denotes that the signal is active low.

The third part registers the function lpt_intr as the interrupt handler. If successful, the variable sc->sc_irq_status is assigned LP_HAS_IRQ, LP_USE_IRQ, and LP_ENABLE_IRQ to indicate that the printer is interrupt-driven. The fourth part allocates memory for two buffers: sc->sc_buf (which will maintain the data to be printed) and sc->sc_buf_stat (which will maintain the printer’s status). Finally, the fifth part creates lpt(4)’s device nodes: lpt%d and lpt%d.ctl, where %d is the unit number. Note that lpt%d.ctl contains the LP_BYPASS flag, while lpt%d does not. In the d_foo functions, LP_BYPASS is used to tell lpt%d.ctl from lpt%d. As you’ll see, the lpt%d device node represents the printer, while lpt%d.ctl is used solely to change the printer’s mode of operation (via lpt(4)’s d_ioctl routine).

lpt_detach Function

The lpt_detach function is the device_detach implementation for lpt(4). Here is its function definition:

static int
lpt_detach(device_t dev)
{
        device_t ppbus = device_get_parent(dev);
        struct lpt_data *sc = device_get_softc(dev);

      destroy_dev(sc->sc_cdev_bypass);
      destroy_dev(sc->sc_cdev);

        ppb_lock(ppbus);
      lpt_release_ppbus(dev);
        ppb_unlock(ppbus);

      callout_drain(&sc->sc_callout);

        if (sc->sc_irq_resource) {
               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_buf_stat, M_DEVBUF);
      free(sc->sc_buf, M_DEVBUF);

        return (0);
}

This function begins by destroying lpt(4)’s device nodes. Once this is done, it relinquishes ownership of the parallel port, drains lpt(4)’s callout function, tears down lpt(4)’s interrupt handler, releases lpt(4)’s IRQ, and frees the allocated memory.

lpt_open Function

The lpt_open function is defined in lpt_cdevsw (that is, lpt(4)’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 lpt_open:

static int
lpt_open(struct cdev *dev, int oflags, int devtype, struct thread *td)
{
        struct lpt_data *sc = dev->si_drv1;
        device_t lpt_dev = sc->sc_dev;
        device_t ppbus = device_get_parent(lpt_dev);
        int try, error;

        if (!sc)
                return (ENXIO);

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

      sc->sc_flags = (uintptr_t)dev->si_drv2;
        if (sc->sc_flags & LP_BYPASS) {
                sc->sc_state = LP_OPEN;
                ppb_unlock(ppbus);
                return (0);
        }

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

        /* Use our IRQ? */
        if (sc->sc_irq_status & LP_ENABLE_IRQ)
                sc->sc_irq_status |= LP_USE_IRQ;
        else
                sc->sc_irq_status &= ˜LP_USE_IRQ;

        /* Reset printer. */
        if ((sc->sc_flags & LP_NO_PRIME) == 0)
                if ((sc->sc_flags & LP_PRIME_OPEN) || sc->sc_primed == 0) {
                      ppb_wctr(ppbus, 0);
                        sc->sc_primed++;
                        DELAY(500);
                }

      ppb_wctr(ppbus, LPC_SEL | LPC_NINIT);

        /* Wait until ready--printer should be running diagnostics. */
        try = 0;
      do {
                /* Give up? */
                if (try++ >= (LPT_INIT_READY * 4)) {
                        lpt_release_ppbus(lpt_dev);
                        sc->sc_state = 0;
                        ppb_unlock(ppbus);
                        return (EBUSY);
                }

                /* Wait 1/4 second. Give up if we get a signal. */
                if (ppb_sleep(ppbus, lpt_dev, LPT_PRI | PCATCH, "lpt_open",
                    hz / 4) != EWOULDBLOCK) {
                        lpt_release_ppbus(lpt_dev);
                        sc->sc_state = 0;
                        ppb_unlock(ppbus);
                        return (EBUSY);
                }
        } while ((ppb_rstr(ppbus) & LP_READY_MASK) != LP_READY);

      sc->sc_control = LPC_SEL | LPC_NINIT;
        if (sc->sc_flags & LP_AUTO_LF)
              sc->sc_control |= LPC_AUTOL;
        if (sc->sc_irq_status & LP_USE_IRQ)
              sc->sc_control |= LPC_ENA;

        ppb_wctr(ppbus, sc->sc_control);

        sc->sc_state &= ˜LP_INIT;
        sc->sc_state |= LP_OPEN;
        sc->sc_transfer_count = 0;

        if (sc->sc_irq_status & LP_USE_IRQ) {
                sc->sc_state |= LP_TIMEOUT;
                sc->sc_ticks = hz / LP_TOUT_INIT;
                callout_reset(&sc->sc_callout, sc->sc_ticks,
                   lpt_timeout, sc);
        }

        lpt_release_ppbus(lpt_dev);
        ppb_unlock(ppbus);

        return (0);
}

This function can be split into six parts. The first checks the value of sc->sc_state. If it does not equal 0, which implies that another process has opened the printer, the error code EBUSY is returned; otherwise, sc->sc_state is assigned LP_INIT. The second part checks the value of dev->si_drv2.

If it contains the LP_BYPASS flag, which indicates that the device node is lpt%d.ctl, sc->sc_state is set to LP_OPEN and lpt_open exits. Recall that lpt%d.ctl is used solely to change the printer’s mode of operation, hence the minute amount of preparatory work. The third part primes the printer and then selects and resets the printer (a printer prepares to receive data when it’s selected, which occurs when the electrical signal at pin 17, dubbed nSELIN, changes from high to low). The fourth part waits for the printer to finish its internal reset. The fifth part selects and resets the printer, enables automatic line feed if requested,[8] and enables interrupts if the printer is interrupt-driven. The fifth part also assigns LP_OPEN to sc->sc_state and zeroes the variable sc->sc_transfer_count.

Note

Automatic line feed is enabled when the electrical signal at pin 14, dubbed nAUTOF, changes from high to low. As you would expect, this causes the printer to automatically insert a line feed after each line.

Finally, the sixth part causes lpt_timeout to execute one time after sc->sc_ticks / hz seconds. The lpt_timeout function is used alongside the interrupt handler lpt_intr. I’ll discuss these functions shortly.

lpt_read Function

The lpt_read function retrieves the printer’s status. Users can get the printer’s status by applying the cat(1) command to the device node lpt%d.

Here is the function definition for lpt_read:

static int
lpt_read(struct cdev *dev, struct uio *uio, int ioflag)
{
        struct lpt_data *sc = dev->si_drv1;
        device_t lpt_dev = sc->sc_dev;
        device_t ppbus = device_get_parent(lpt_dev);
        int num, error = 0;

      if (sc->sc_flags & LP_BYPASS)
                return (EPERM);

        ppb_lock(ppbus);
        error = ppb_1284_negociate(ppbus, PPB_NIBBLE, 0);
        if (error) {
                ppb_unlock(ppbus);
                return (error);
        }

        num = 0;
        while (uio->uio_resid) {
                error = ppb_1284_read(ppbus, PPB_NIBBLE,
 sc->sc_buf_stat,
                    min(BUF_STAT_SIZE, uio->uio_resid), &num);
                if (error)
                        goto end_read;

              if (!num)
                        goto end_read;

                ppb_unlock(ppbus);
                error = uiomove(sc->sc_buf_stat, num, uio);
                ppb_lock(ppbus);
                if (error)
                        goto end_read;
        }

end_read:
        ppb_1284_terminate(ppbus);
        ppb_unlock(ppbus);
        return (error);
}

This function first checks the value of sc->sc_flags. If it contains the LP_BYPASS flag, which indicates that the device node is lpt%d.ctl, the error code EPERM (which stands for error: operation not permitted) is returned. Next, the function ppb_1284_negociate is called to put the parallel port interface into nibble mode.

Note

Nibble mode is the most common way to retrieve data from a printer. Normally, pins 10, 11, 12, 13, and 15 are used by the printer as external status indicators; however, in nibble mode these pins are used to send data to the host (4 bits at a time).

The remainder of this function transfers data from the printer to user space. The data in this case is the printer’s status. Here, ppb_1284_read transfers data from the printer to kernel space. The number of bytes transferred is saved in num. If num equals 0, lpt_read exits. The uiomove function then moves the data from kernel space to user space.

lpt_write Function

The lpt_write function acquires data from user space and stores it in sc->sc_buf. This data is then sent to the printer to be printed.

Here is the function definition for lpt_write:

static int
lpt_write(struct cdev *dev, struct uio *uio, int ioflag)
{
        struct lpt_data *sc = dev->si_drv1;
        device_t lpt_dev = sc->sc_dev;
        device_t ppbus = device_get_parent(lpt_dev);
        register unsigned num;
        int error;

        if (sc->sc_flags & LP_BYPASS)
                return (EPERM);

        ppb_lock(ppbus);
        error = lpt_request_ppbus(lpt_dev, PPB_WAIT | PPB_INTR);
        if (error) {
                ppb_unlock(ppbus);
                return (error);
        }

      sc->sc_state &= ˜LP_INTERRUPTED;
        while ((num = min(BUF_SIZE, uio->uio_resid))) {
                sc->sc_cp = sc->sc_buf;

                ppb_unlock(ppbus);
                error = uiomove(sc->sc_cp, num, uio);
                ppb_lock(ppbus);
                if (error)
                        break;

              sc->sc_transfer_count = num;

              if (sc->sc_irq_status & LP_ENABLE_EXT) {
                        error = ppb_write(ppbus, sc->sc_cp,
                            sc->sc_transfer_count, 0);
                        switch (error) {
                        case 0:
                                sc->sc_transfer_count = 0;
                                break;
                        case EINTR:
                                sc->sc_state |= LP_INTERRUPTED;
                                ppb_unlock(ppbus);
                                return (error);
                        case EINVAL:
                                log(LOG_NOTICE,
                                    "%s: extended mode not available
",
                                    device_get_nameunit(lpt_dev));
                                break;
                        default:
                                ppb_unlock(ppbus);
                                return (error);
                        }
                } else while ((sc->sc_transfer_count > 0) &&
                             (sc->sc_irq_status & LP_USE_IRQ)) {
                        if (!(sc->sc_state & LP_BUSY))
                              lpt_intr(sc);

                        if (sc->sc_state & LP_BUSY) {
                                error = ppb_sleep(ppbus, lpt_dev,
                                    LPT_PRI | PCATCH, "lpt_write", 0);
                                if (error) {
                                        sc->sc_state |= LP_INTERRUPTED;
                                        ppb_unlock(ppbus);
                                        return (error);
                                }
                        }
                }

                if (!(sc->sc_irq_status & LP_USE_IRQ) &&
                     (sc->sc_transfer_count)) {
                        error = lpt_push_bytes(sc);
                        if (error) {
                                ppb_unlock(ppbus);
                                return (error);
                        }
                }
        }

        lpt_release_ppbus(lpt_dev);
        ppb_unlock(ppbus);

        return (error);
}

Like lpt_read, this function starts by checking the value of sc->sc_flags. If it contains the LP_BYPASS flag, the error code EPERM is returned. Next, the LP_INTERRUPTED flag is removed from sc->sc_state (as you’ll see, LP_INTERRUPTED is added to sc->sc_state whenever a write operation is interrupted). The following while loop contains the bulk of lpt_write. Note that its expression determines the amount of data to copy from user space to kernel space. This amount is saved in sc->sc_transfer_count, which is decremented each time a byte is sent to the printer.

Now, there are three ways to transfer data from kernel space to the printer. First, if extended mode is enabled, lpt_write can write directly to the printer.

Note

Extended mode refers to either Enhanced Parallel Port (EPP) or Extended Capabilities Port (ECP) mode. EPP and ECP modes are designed to transmit data faster and with less CPU overhead than normal parallel port communications. Most parallel ports support one or both of these modes.

Second, if the printer is interrupt-driven and the LP_BUSY flag is cleared in sc->sc_state, lpt_write can call lpt_intr to transfer data to the printer. Looking at the function definition for lpt_intr in the following section, you’ll see that LP_BUSY is set during lpt_intr’s execution, and that LP_BUSY is not cleared until sc->sc_transfer_count is 0. This prevents lpt_write from issuing another interrupt-driven transfer until the current one completes, which is why lpt_write sleeps.

Finally, if the first and second options are unavailable, lpt_write can issue a polled transfer by calling lpt_push_bytes, which is described in lpt_push_bytes Function in lpt_close Function.

lpt_intr Function

The lpt_intr function is lpt(4)’s interrupt handler. This function transfers 1 byte from sc->sc_buf to the printer and then it exits. When the printer is ready for another byte, it will send an interrupt. Note that in lpt_intr, sc->sc_buf is accessed via sc->sc_cp.

Here is the function definition for lpt_intr:

static void
lpt_intr(void *arg)
{
        struct lpt_data *sc = arg;
        device_t lpt_dev = sc->sc_dev;
        device_t ppbus = device_get_parent(lpt_dev);
        int i, status = 0;

      for (i = 0; i < 100 &&
             ((status = ppb_rstr(ppbus)) & LP_READY_MASK) != LP_READY; i++)
                ;       /* nothing. */

        if ((status & LP_READY_MASK) == LP_READY) {
              sc->sc_state = (sc->sc_state | LP_BUSY) & ˜LP_ERROR;
              sc->sc_ticks = hz / LP_TOUT_INIT;

                if (sc->sc_transfer_count) {
                      ppb_wdtr(ppbus, *sc->sc_cp++);
                      ppb_wctr(ppbus, sc->sc_control | LPC_STB);
                        ppb_wctr(ppbus, sc->sc_control);

                        if (--(sc->sc_transfer_count) > 0)
                               return;
                }

              sc->sc_state &= ˜LP_BUSY;

                if (!(sc->sc_state & LP_INTERRUPTED))
                      wakeup(lpt_dev);

                return;
        } else {
                if (((status & (LPS_NERR | LPS_OUT)) != LPS_NERR) &&
                    (sc->sc_state & LP_OPEN))
                        sc->sc_state |= LP_ERROR;
        }
}

This function first checks ad nauseam that the printer is online and ready for output. If it is, the LP_BUSY flag is added to sc->sc_state and the LP_ERROR flag, which denotes a printer error, is removed. Next, sc->sc_ticks is reset. Then 1 byte from sc->sc_buf is written to the parallel port’s data register and subsequently sent to the printer (data on the parallel port interface is sent to the printer when the electrical signal at pin 1, dubbed nSTROBE, changes from high to low). If there is more data to send (that is, sc->sc_transfer_count is greater than 0), lpt_intr exits, because it is protocol to wait for an interrupt before sending another byte. If there is no more data to send, LP_BUSY is cleared from sc->sc_state and lpt_write is woken up.

lpt_timeout Function

The lpt_timeout function is the callout function for lpt(4). It is designed to deal with missed or unhandled interrupts. Here is its function definition:

static void
lpt_timeout(void *arg)
{
        struct lpt_data *sc = arg;
        device_t lpt_dev = sc->sc_dev;

      if (sc->sc_state & LP_OPEN) {
                sc->sc_ticks++;
                if (sc->sc_ticks > hz / LP_TOUT_MAX)
                        sc->sc_ticks = hz / LP_TOUT_MAX;
              callout_reset(&sc->sc_callout, sc->sc_ticks,
                    lpt_timeout, sc);
        } else
                sc->sc_state &= ˜LP_TIMEOUT;

        if (sc->sc_state & LP_ERROR)
              sc->sc_state &= ˜LP_ERROR;

      if (sc->sc_transfer_count)
              lpt_intr(sc);
        else {
                sc->sc_state &= ˜LP_BUSY;
                wakeup(lpt_dev);
        }
}

This function first checks whether lpt%d is open. If so, lpt_timeout reschedules itself to execute. Next, LP_ERROR is removed from sc->sc_state. Now if lpt(4) has missed an interrupt, lpt_intr is called to restart transferring data to the printer.

Note that without the if block at , lpt(4) would hang waiting for an interrupt that’s been sent and lost.

lpt_push_bytes Function

The lpt_push_bytes function uses polling to transfer data to the printer. This function is called (by lpt_write) only if extended mode is disabled and the printer is not interrupt-driven.

Here is the function definition for lpt_push_bytes:

static int
lpt_push_bytes(struct lpt_data *sc)
{
        device_t lpt_dev = sc->sc_dev;
        device_t ppbus = device_get_parent(lpt_dev);
        int error, spin, tick;
        char ch;

      while (sc->sc_transfer_count > 0) {
                ch = *sc->sc_cp;
                sc->sc_cp++;
                sc->sc_transfer_count--;

              for (spin = 0; NOT_READY(ppbus) && spin < MAX_SPIN; spin++)
                        DELAY(1);

                if (spin >= MAX_SPIN) {
                        tick = 0;
                        while (NOT_READY(ppbus)) {
                                tick = tick + tick + 1;
                                if (tick > MAX_SLEEP)
                                        tick = MAX_SLEEP;

                                error = ppb_sleep(ppbus, lpt_dev, LPT_PRI,
                                    "lpt_poll", tick);
                                if (error != EWOULDBLOCK)
                                        return (error);
                        }
                }

              ppb_wdtr(ppbus, ch);
              ppb_wctr(ppbus, sc->sc_control | LPC_STB);
                ppb_wctr(ppbus, sc->sc_control);
        }

        return (0);
}

This function first verifies that there is data to transfer. Then it polls the printer to see if it is online and ready for output. If the printer is not ready, lpt_push_bytes sleeps for a short period of time and then repolls the printer when it wakes up. This cycle of sleeping and polling is repeated until the printer is ready. If the printer is ready, 1 byte from sc->sc_buf is written to the parallel port’s data register and then sent to the printer. This entire process is repeated until all of the data in sc->sc_buf is transferred.

lpt_close Function

The lpt_close function is defined in lpt_cdevsw as the d_close operation. Here is its function definition:

static int
lpt_close(struct cdev *dev, int fflag, int devtype, struct thread *td)
{
        struct lpt_data *sc = dev->si_drv1;
        device_t lpt_dev = sc->sc_dev;
        device_t ppbus = device_get_parent(lpt_dev);
        int error;

        ppb_lock(ppbus);

      if (sc->sc_flags & LP_BYPASS)
                goto end_close;

        error = lpt_request_ppbus(lpt_dev, PPB_WAIT | PPB_INTR);
        if (error) {
                ppb_unlock(ppbus);
                return (error);
        }

      if (!(sc->sc_state & LP_INTERRUPTED) &&
           (sc->sc_irq_status & LP_USE_IRQ))
                while ((ppb_rstr(ppbus) & LP_READY_MASK) != LP_READY ||
                   sc->sc_transfer_count)
                        if (ppb_sleep(ppbus, lpt_dev, LPT_PRI | PCATCH,
                            "lpt_close", hz) != EWOULDBLOCK)
                                break;

      sc->sc_state &= ˜LP_OPEN;
      callout_stop(&sc->sc_callout);
      ppb_wctr(ppbus, LPC_NINIT);

        lpt_release_ppbus(lpt_dev);

 end_close:
      sc->sc_state = 0;
      sc->sc_transfer_count = 0;
        ppb_unlock(ppbus);
        return (0);
}

Like lpt_read and lpt_write, this function first checks the value of sc->sc_flags. If it contains the LP_BYPASS flag, lpt_close jumps to end_close. Next, lpt(4) is assigned ownership of the parallel port. The following if block ensures that if there is still data to transfer and the printer is interrupt-driven, the transfer is completed before closing lpt%d. Then, LP_OPEN is removed from sc->sc_state, lpt_timeout is stopped, the printer is reset, and ownership of the parallel port is relinquished. Lastly, sc->sc_state and sc->sc_transfer_count are zeroed.

lpt_ioctl Function

The lpt_ioctl function is defined in lpt_cdevsw as the d_ioctl operation. Before I describe this function, an explanation of its ioctl command, LPT_IRQ, is needed. LPT_IRQ is defined in the <dev/ppbus/lptio.h> header as follows:

#define LPT_IRQ         _IOW('p', 1, long)

As you can see, LPT_IRQ requires a long int value.

static int
lpt_ioctl(struct cdev *dev, u_long cmd, caddr_t data, int fflag,
    struct thread *td)
{
        struct lpt_data *sc = dev->si_drv1;
        device_t lpt_dev = sc->sc_dev;
        device_t ppbus = device_get_parent(lpt_dev);
        u_short old_irq_status;
        int error = 0;

        switch (cmd) {
      case LPT_IRQ:
                ppb_lock(ppbus);
                if (sc->sc_irq_status & LP_HAS_IRQ) {
                        old_irq_status = sc->sc_irq_status;
                        switch (*(int *)data) {
                        case 0:
                              sc->sc_irq_status &= ˜LP_ENABLE_IRQ;
                                break;
                        case 1:
                                sc->sc_irq_status &= ˜LP_ENABLE_EXT;
                              sc->sc_irq_status |= LP_ENABLE_IRQ;
                                break;
                        case 2:
                                sc->sc_irq_status &= ˜LP_ENABLE_IRQ;
                              sc->sc_irq_status |= LP_ENABLE_EXT;
                                break;
                        case 3:
                              sc->sc_irq_status &= ˜LP_ENABLE_EXT;
                                break;
                        default:
                                break;
                        }

                        if (old_irq_status != sc->sc_irq_status)
                                log(LOG_NOTICE,
                                    "%s: switched to %s %s mode
",
                                    device_get_nameunit(lpt_dev),
                                    (sc->sc_irq_status & LP_ENABLE_IRQ) ?
                                    "interrupt-driven" : "polled",
                                    (sc->sc_irq_status & LP_ENABLE_EXT) ?
                                    "extended" : "standard");
                } else
                        error = EOPNOTSUPP;

                ppb_unlock(ppbus);
                break;
        default:
                error = ENODEV;
                break;
        }

        return (error);
}

Based on the argument given to LPT_IRQ, lpt_ioctl either disables interrupt-driven mode (which enables polled mode), enables interrupt-driven mode, enables extended mode, or disables extended mode (which enables standard mode). Note that interrupt-driven mode and extended mode conflict with each other, so if one is enabled, the other is disabled.

Note

To run this function, you’d use the lptcontrol(8) utility, whose source code I suggest you take a quick look at.

lpt_request_ppbus Function

The lpt_request_ppbus function sets lpt(4) as the owner of the parallel port. Recall that owning the parallel port lets a device (such as lpt%d) transfer data to and from it.

Here is the function definition for lpt_request_ppbus:

static int
lpt_request_ppbus(device_t dev, int how)
{
        device_t ppbus = device_get_parent(dev);
        struct lpt_data *sc = device_get_softc(dev);
        int error;

        ppb_assert_locked(ppbus);

      if (sc->sc_state & LP_HAVEBUS)
               return (0);

        error = ppb_request_bus(ppbus, dev, how);
        if (!error)
               sc->sc_state |= LP_HAVEBUS;

        return (error);
}

This function begins by checking the value of sc->sc_state. If it contains LP_HAVEBUS, which indicates that lpt(4) currently owns the parallel port, lpt_request_ppbus exits. Otherwise, ppb_request_bus is called to set lpt(4) as the owner of the parallel port and sc->sc_state is assigned LP_HAVEBUS.

lpt_release_ppbus Function

The lpt_release_ppbus function causes lpt(4) to relinquish ownership of the parallel port. Here is its function definition:

static int
lpt_release_ppbus(device_t dev)
{
        device_t ppbus = device_get_parent(dev);
        struct lpt_data *sc = device_get_softc(dev);
        int error = 0;

        ppb_assert_locked(ppbus);

      if (sc->sc_state & LP_HAVEBUS) {
                error = ppb_release_bus(ppbus, dev);
                if (!error)
                       sc->sc_state &= ˜LP_HAVEBUS;
        }

        return (error);
}

This function first verifies that lpt(4) currently owns the parallel port. Next, it calls ppb_release_bus to relinquish ownership of the parallel port. Then LP_HAVEBUS is removed from sc->sc_state.



[8] Curiously enough, it’s currently impossible to request automatic line feed.

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

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