Writing Device Drivers

Synchronous Data Transfers

This section presents a simple method for performing synchronous I/O transfers. It assumes that the hardware is a simple disk device that can transfer only one data buffer at a time using DMA, and that the disk can be spun up and spun down by software command. The device driver's strategy(9E) routine waits for the current request to be completed before accepting a new one. The device interrupts when the transfer is complete or when an error occurs.

  1. Check for invalid buf(9S) requests.

    Check the buf(9S) structure passed to strategy(9E) for validity. All drivers should check that:

    1. The request begins at a valid block. The driver converts the b_blkno field to the correct device offset and then determines if the offset is valid for the device.

    2. The request does not go beyond the last block on the device.

    3. Device-specific requirements are met.

    If an error is encountered, the driver should indicate the appropriate error with bioerror(9F) and complete the request by calling biodone(9F). biodone(9F) notifies the caller of strategy(9E) that the transfer is complete (in this case, because of an error).

  2. Check whether the device is busy.

    Synchronous data transfers allow single-threaded access to the device. The device driver enforces this by maintaining a busy flag (guarded by a mutex), and by waiting on a condition variable with cv_wait(9F) when the device is busy.

    If the device is busy, the thread waits until a cv_broadcast(9F) or cv_signal(9F) from the interrupt handler indicates that the device is no longer busy. See Chapter 4, Multithreading, for details on condition variables.

    When the device is no longer busy, the strategy(9E) routine marks it as busy and prepares the buffer and the device for the transfer.

  3. Check whether the device is spun up; if it is not, arrange for it to be spun up.

  4. Set up the buffer for DMA.

    Prepare the data buffer for a DMA transfer by allocating a DMA handle using ddi_dma_alloc_handle(9F) and binding the data buffer to the handle using ddi_dma_buf_bind_handle(9F). See Chapter 7, DMA, for information on setting up DMA resources and related data structures.

  5. Begin the transfer.

    At this point, a pointer to the buf(9S) structure is saved in the state structure of the device. This is so that the interrupt routine can complete the transfer by calling biodone(9F).

    The device driver then accesses device registers to initiate a data transfer. In most cases, the driver should protect the device registers from other threads by using mutexes. In this case, because strategy(9E) is single-threaded, guarding the device registers is not necessary. See Chapter 4, Multithreading, for details about data locks.

    Once the executing thread has started the device's DMA engine, the driver can return execution control to the calling routine. (See Example 10-4.)


    Example 10-4 Synchronous Block Driver strategy(9E) Routine

    static int
    xxstrategy(struct buf *bp)
    {
    	struct xxstate *xsp;
    	struct device_reg *regp;
    	minor_t instance;
    	ddi_dma_cookie_t cookie;
    	instance = getminor(bp->b_edev);
    	xsp = ddi_get_soft_state(statep, instance);
    	if (xsp == NULL) {
    	   	bioerror(bp, ENXIO);
    	   	biodone(bp);
    	   	return (0);
    	}
    	/* validate the transfer request */
    	if ((bp->b_blkno >= xsp->nblocks) || (bp->b_blkno < 0)) {
    	   	bioerror(bp, EINVAL);	
    	   	biodone(bp);
    	   	return (0);
    	}
    	/*
    	 * Hold off all threads until the device is not busy.
    	 */
    	mutex_enter(&xsp->mu);
    	while (xsp->busy) {
    		   cv_wait(&xsp->cv, &xsp->mu);
    	}
    	xsp->busy = 1;
    	mutex_exit(&xsp->mu);
    	if the device has power manageable components (see Chapter 8, Power Management),
    	mark the device busy with pm_busy_components(9F)
    , and then ensure that the device 
    	is powered up by calling ddi_dev_is_needed(9F).
    
    	Set up DMA resources with ddi_dma_alloc_handle(9F) 
    	and ddi_dma_buf_bind_handle(9F). 
    
    	xsp->bp = bp;
    	regp = xsp->regp;
    	ddi_put32(xsp->data_access_handle, &regp->dma_addr,
    			cookie.dmac_address);
    	ddi_put32(xsp->data_access_handle, &regp->dma_size,
    			 (uint32_t)cookie.dmac_size);
    	ddi_put8(xsp->data_access_handle, &regp->csr,
    			 ENABLE_INTERRUPTS | START_TRANSFER);
    	return (0);
    }

  6. Handle the interrupting device.

    When the device finishes the data transfer it generates an interrupt, which eventually results in the driver's interrupt routine being called. Most drivers specify the state structure of the device as the argument to the interrupt routine when registering interrupts (see ddi_add_intr(9F) and "Registering Interrupts"). The interrupt routine can then access the buf(9S) structure being transferred, plus any other information available from the state structure.

    The interrupt handler should check the device's status register to determine if the transfer completed without error. If an error occurred, the handler should indicate the appropriate error with bioerror(9F). The handler should also clear the pending interrupt for the device and then complete the transfer by calling biodone(9F).

    As the final task, the handler clears the busy flag and calls cv_signal(9F) or cv_broadcast(9F) on the condition variable, signaling that the device is no longer busy. This allows other threads waiting for the device (in strategy(9E)) to proceed with the next data transfer.


    Example 10-5 Synchronous Block Driver Interrupt Routine

    static u_int
    xxintr(caddr_t arg)
    {
    	struct xxstate *xsp = (struct xxstate *)arg;
    	struct buf *bp;
    	uint8_t status;
    	mutex_enter(&xsp->mu);
    	status = ddi_get8(xsp->data_access_handle, &xsp->regp->csr);
    	if (!(status & INTERRUPTING)) {
    	   	mutex_exit(&xsp->mu);
    	   	return (DDI_INTR_UNCLAIMED);
    	}
    	/* Get the buf responsible for this interrupt */
    	bp = xsp->bp;
    	xsp->bp = NULL;
    	/*
    	 * This example is for a simple device which either
    	 * succeeds or fails the data transfer, indicated in the
    	 * command/status register.
    	 */
    	if (status & DEVICE_ERROR) {
    	   	/* failure */
    	   	bp->b_resid = bp->b_bcount;
    	   	bioerror(bp, EIO);
    	} else {
    	   	/* success */
    	   	bp->b_resid = 0;
    	}
    	ddi_put8(xsp->data_access_handle, &xsp->regp->csr,
    	   	CLEAR_INTERRUPT);
    	/* The transfer has finished, successfully or not */
    	biodone(bp);
    	if the device has power manageable components that were marked busy in strategy(9F).
    	mark them idle now with pm_idle_component(9F)
    	release any resources used in the transfer, such as DMA resources 
    	(ddi_dma_unbind_handle(9F) and ddi_dma_free_handle(9F)).
    
    	/* Let the next I/O thread have access to the device */
    	xsp->busy = 0;
    	cv_signal(&xsp->cv);
    	mutex_exit(&xsp->mu);
    	return (DDI_INTR_CLAIMED);
    }