Sometimes you must write a driver for a device that does not support DMA. Even if the device does support DMA, you may not want to use DMA to transfer amounts of data so small as not to warrant the DMA overhead.
In these cases, the host processor usually copies the data from the user space to on-board memory. Your driver can then program the device registers to notify the device that the memory is ready. The device controller can then copy the data from its on-board memory to the peripheral device.
Listed below is part of a mythical VME device driver for a printer controller that does not support DMA.
To print data from the user, the driver copies data from the user's buffer to an on-board memory buffer of size VDK_MEMSIZE. Following the copy of each chunk, the driver programs the device register to indicate the size of valid data in memory and to tell the controller to start printing.
The driver then sleeps, waiting for an interrupt to indicate that the printing is complete and that the on-board memory buffer is available again. To prevent a race condition, in which the interrupt responds before the calling process can sleep, the driver uses the splvme() and splx() routines. See the spl(D3) man page.
int vdk_state; /* flag for transfer state */ int vdkwrite(dev_t dev, uio_t *uiop, cred_t *crp) { register int size; register int i; int s; int err; /* while there is data to transfer */ while( uiop->uio_resid > 0 ) { /* can only move VDK_MEMSIZE bytes at a time */ size = MIN(uiop->uio_resid,VDK_MEMSIZE); if( (err = uiomove(vdk_memory,size,UIO_WRITE,uiop)) != 0 ) return err; /* block interrupts until we sleep */ s = splvme(); /* may not be sufficient on MP */ /* start printing */ vdk_device->count = size; vdk_device->command = VDK_GO; vdk_state = VDK_SLEEPING; while ( vdk_state != VDK_DONE ) sleep(&vdk_state,PRIBIO); /* restore the process level after waking up */ splx(s); /* clears any MP locks as well */ } return 0; } void vdkintr(int unit) { ... /* printing is complete */ if( vdk_state == VDK_SLEEPING ) { vdk_state = VDK_DONE; wakeup(&vdk_state); } ... }The driver's use of the volatile declaration informs the optimizer that this variable points to a hardware value that may change. Otherwise, the optimizer may determine that one write to vdk_device->command or storage of the value in a register is sufficient.
Note: If your driver uses the sleep() and wakeup() kernel routines to sleep and awaken, it is a good idea for the top half to verify that the event has occurred before awakening the sleeping process. (See sleep(D3) for details on the sleep/wakeup process synchronization mechanism.) If your driver uses the biowait()/biodone() routines or the psema()/vsema() routines to sleep and awaken, you need not worry about its awakening by accident. However, the routines psema() and vsema() are specific to IRIX and are probably not supported on other operating systems. The uiomove() kernel routine is a useful procedure to call in these situations because it automatically updates uio and iovec structures while checking for valid user addresses. Remember that uiop->uio_resid must be left with the number of bytes remaining untransferred.
Note: uiomove() uses bcopy() to transfer data. bcopy() transfers data as fast as possible between locations in system memory. bcopy() takes advantage of CPU-specific commands to optimize performance. On the R4000 processor, bcopy() tries to move eight bytes at a time. Most VME-bus boards cannot move data eight bytes at a time, so using this routine directly may not work. A work-around would be to use uiomove() to copy the data from a user buffer into a kernel buffer, then to use pio_bcopy() to copy the data from the kernel buffer to the VME-bus board. pio_bcopy() allows the user to specify the element size being transferred.