Writing Device Drivers

Miscellaneous I/O Control

The ioctl(9E) routine is called when a user thread issues an ioctl(2) system call on a file descriptor associated with the device. The I/O control mechanism is a catchall for getting and setting device-specific parameters. It is frequently used to set a device-specific mode, either by setting internal driver software flags or by writing commands to the device. It can also be used to return information to the user about the current device state. In short, it can do whatever the application and driver need it to do.

ioctl(9E)

int xxioctl(dev_t dev, int cmd, intptr_t arg, int mode,
 	cred_t *credp, int *rvalp);

The cmd parameter indicates which command ioctl(9E) should perform. By convention, I/O control commands indicate the driver they belong to in bits 8-15 of the command (usually given by the ASCII code of a character representing the driver), and the driver-specific command in bits 0-7. They are usually created in the following way:

	#define XXIOC		(`x' << 8)		/* `x' is a character representing */
 											/* device xx */
	#define XX_GET_STATUS				(XXIOC | 1) /* get status register */
 #define XX_SET_CMD					(XXIOC | 2) /* send command */

The interpretation of arg depends on the command. I/O control commands should be documented (in the driver documentation, or a manual page) and defined in a public header file, so that applications can determine the names, what they do, and what they accept or return as arg. Any data transfer of arg (into or out of the driver) must be performed by the driver.

Drivers must use ddi_copyin(9F) to transfer "arg" data from the userland application to the kernel and ddi_copyout(9F) from kernel to userland. Failure to use ddi_copyin(9F) or ddi_copyout(9F) will result in panics on architectures that separate kernel and user address spaces, or if the user address has been swapped out.

ioctl(9E) is usually a switch statement with a case for each supported ioctl(9E) request.


Example 9-13 ioctl(9E) Routine

static int
xxioctl(dev_t dev, int cmd, intptr_t arg, int mode,
	cred_t *credp, int *rvalp)
{
 	uint8_t				csr;
 	struct xxstate 		*xsp;

 	xsp = ddi_get_soft_state(statep, getminor(dev));
 	if (xsp == NULL) {
	    	return (ENXIO);
 	}
 	switch (cmd) {
 	case XX_GET_STATUS:
		   csr = ddi_get8(xsp->data_access_handle, &xsp->regp->csr);
	   	if (ddi_copyout(&csr, (void *)arg,
				      sizeof (uint8_t), mode) != 0) {
			   return (EFAULT);
		   }
	   	break;
 	case XX_SET_CMD:
	     if (ddi_copyin((void *)arg, &csr,
				      sizeof (uint8_t), mode) != 0) {
			   return (EFAULT);
	   	}
	   	ddi_put8(xsp->data_access_handle, &xsp->regp->csr, csr);
	    	break;
 	default:
	   	/* generic "ioctl unknown" error */
	   	return (ENOTTY);
 	}
 	return (0);
}

The cmd variable identifies a specific device control operation. If arg contains a user virtual address, ioctl(9E) must call ddi_copyin(9F) or ddi_copyout(9F) to transfer data between the data structure in the application program pointed to by arg and the driver. In Example 9-13, for the case of an XX_GET_STATUS request the contents of xsp->regp->csr are copied to the address in arg. When a request succeeds, ioctl(9E) can store in *rvalp any integer value to be the return value of the ioctl(2) system call that made the request. Negative return values, such as -1, should be avoided, as they usually indicate the system call failed, and many application programs assume that negative values indicate failure.

An application that uses the I/O controls discussed above could look like Example 9-14.


Example 9-14 Using ioctl(9E)

#include <sys/types.h>
#include "xxio.h"				/* contains device's ioctl cmds and arguments */
int
main(void)
{
 	uint8_t		status;
 	...

 	/*
	    * read the device status
	    */
 	if (ioctl(fd, XX_GET_STATUS, &status) == -1) {
	    	 error handling
 	}
 	printf("device status %x\n", status);
 	exit(0);
}

I/O Control Support for 64-Bit Capable Device Drivers

The Solaris kernel runs in 64-bit mode on suitable hardware and supports both 32-bit and 64-bit applications. A 64-bit device driver is required to support I/O control commands from 32-bit and 64-bit user mode programs. The difference between a 32-bit program and a 64-bit program is its C language type model: a 32-bit program is ILP32 and a 64-bit program is LP64. See Appendix F, Making a Device Driver 64-Bit Ready , for information on C data type models.

Any data that flows between programs and the kernel and vice versa (for example using ddi_copyin(9F) or ddi_copyout(9F)) will either need to be identical in format regardless of the type model of the kernel and application, or the device driver should be able to handle a model mismatch between it and the application and adjust the data format accordingly.

To determine if there is a model mismatch, the ioctl(9E) mode parameter passes the data model bits to the driver. As Example 9-15 shows, the mode parameter is then passed to ddi_model_convert_from(9F) to determine if any model conversion is necessary.

In the following example, the driver copies a data structure which contains a user address. Because the data structure changes size from ILP32 to LP64, the 64-bit driver uses a 32-bit version of the structure when communicating with a 32-bit application.


Example 9-15 ioctl(9E) Routine to Support 32-bit and 64-bit Applications

struct args32 {
 	uint32_t addr;	/* 32-bit address in LP64 */
	 int len;
}
struct args {
	   caddr_t addr;	/* 64-bit address in LP64 */
 	int len;
}

static int
xxioctl(dev_t dev, int cmd, intptr_t arg, int mode,
	cred_t *credp, int *rvalp)
{
	   struct xxstate *xsp;
 	struct args a;
 	xsp = ddi_get_soft_state(statep, getminor(dev));
 	if (xsp == NULL) {
		    return (ENXIO);
 	}
 	switch (cmd) {
 	case XX_COPYIN_DATA:
		   switch(ddi_model_convert_from(mode & FMODELS)) {
		   case DDI_MODEL_ILP32:
		   {
			      struct args32 a32;

			      /* copy 32-bit args data shape */
			      if (ddi_copyin((void *)arg, &a32,
				      sizeof (struct args32), mode) != 0) {
				    	return (EFAULT);
			       }
			       /* convert 32-bit to 64-bit args data shape */
			       a.addr = a32.addr;
			       a.len = a32.len;
			       break;
		   }
		   case DDI_MODEL_NONE:
			       /* application and driver have same data model. */
			       if (ddi_copyin((void *)arg, &a, sizeof (struct args),
				            mode) != 0) {
					  return (EFAULT);
			        }
		   }
		   /* continue using data shape in native driver data model. */
		   break;

 	case XX_COPYOUT_DATA:
	    	/* copyout handling */
	    	break;
 	default:
	    	/* generic "ioctl unknown" error */
	    	return (ENOTTY);
 	}
	   return (0);
}