Writing Device Drivers

General Conversion Steps

The sections below provide information on converting drivers to run in a 64-bit environment. Driver writers may need to do one or more of the following:

Convert Driver Code to Be 64-Bit Clean

As a first step in converting a device driver to run in a 64-bit kernel, make the driver 64-bit clean by converting to the LP64 data model. For example, make sure that the driver's use of long variables remains consistent between data models.

To enable source code to be both 32-bit and 64-bit safe, the Solaris 2.6 system provides new fixed-width integer types, derived types, constants, and macros in the files <sys/types.h> and <sys/inttypes.h>. The fixed-width types include both signed and unsigned integer types such as int8_t, uint8_t, uint32_t, uint64_t, as well as constants that specify their limits.

Note the following when converting to LP64:

  1. System-derived types, such as size_t, should be used for type declarations whenever possible.

    Using derived types will help make driver code 32-bit and 64-bit safe, since the derived types themselves are safe for both IPL32 and LP64 data models. In addition, it is a good idea to use system-derived types for definitions to allow for future change.

  2. Fixed-width types, such as uint32_t, should be used where appropriate to clearly specify type declarations.

    Fixed-width integer types are useful for representing explicit sizes of binary data structures or hardware registers, while fundamental C language data types, such as int, can still be used for loop counters or file descriptors. In particular, make sure that use of variables of type long remains accurate. As a long is 64 bits in LP64, change variables that represent 32-bit data and that are currently defined as long to fixed-width 32-bit types, such as uint32_t.

  3. The new derived types uintptr_t or intptr_t should be used as the integral type for pointers.

    Pointers are 64 bits in size in the Solaris 64-bit environment. Although pointers could be recast to long, the new derived system types should be used instead, particularly when pointers are used to do address arithmetic or alignment. In addition, check for code that assumes that int and pointer variables are the same size, and use uintptr_t instead of int for variables that are cast to pointers. For example, change:

    		char *p;
     	p = (char *)((int)p & PAGEOFFSET);

    to:

    		p = (char *)((uintptr_t)p & PAGEOFFSET);

The new fixed width data types in <sys/inttypes.h> are included in <sys/ddi.h>.


Caution - Caution -

Caution: Do not ignore compilation or lint warnings during conversion for LP64, because even those that were safe to ignore previously in the ILP32 environment may now indicate a more serious problem.


Useful Tools to Check Data Model Conversion

Running driver code through the Sun WorkShop Compiler C 4.2 lint utility can help find problems with data models. This version of lint provides warnings about potential 64-bit problems. It prints the line number of the problem code and a warning message describing the problem, indicates whether a pointer was involved, and provides information about the size of the data types.

To use lint(1B) to check for data model conversion problems, use the -errchk=longptr64 option.

Update Data Structures to Preserve 32-Bit Data in Register Layouts

In the 64-bit data model, data structures that use long to define the type of arguments might be incorrect if the argument needs to define a 32-bit quantity. For example, some drivers currently use long to define 32-bit fields in a hardware register layout. To make a driver 64-bit safe, update data structures where necessary to use int32_t or uint32_t, defined in <sys/inttypes.h>, instead of long for 32-bit data. This preserves the binary layout of 32-bit data structures.

For example, change:

struct reg {
 	ulong_t			addr;
 	uint_t			count;
}

to:

struct reg {
 	uint32_t			addr;
 	uint32_t			count;
}

Check Use of Derived Types That Change Size Between 32-Bit and 64-Bit Environments

System derived types, such as size_t, should be used where possible so that the resulting variables make sense when passed between functions. The new derived types uintptr_t or intptr_t should be used as the integral type for pointers.

Fixed-width integer types are useful for representing explicit sizes of binary data structures or hardware registers, while fundamental C language data types, such as int, can still be used for loop counters or file descriptors.

Some system derived types represent 32-bit quantities on a 32-bit system but represent 64-bit quantities on a 64-bit system. Derived types that change size in this way include:

Drivers that use these derived types should pay particular attention to their use, particularly if they are assigning these values to variables of another derived type, such as a fixed-width type.

Change Common Access Functions to Fixed-Width Versions

Previously, DDI common access functions specified the size of data in terms of bytes, words, and so on. For example, ddi_getl(9F) was used to access 32-bit quantities. This function will not work to access a 32-bit quantity in the 64-bit environment because long is now a 64-bit quantity.

In Solaris 2.6, new common access functions that use fixed-width types have been added. These functions have been named to reflect the actual data size. For example, in a 64-bit environment, a driver must use ddi_get32(9F) to access 32-bit data rather than ddi_getl(9F).

	uint32_t ddi_get32(ddi_acc_handle_t hdl, uint32_t *dev_addr); 

To make a device driver 64-bit safe, replace all Common Access functions with the new fixed-width versions.

Table F-2 shows a subset of the new common access functions. For a complete list of the new functions, see Appendix B, Interface Transition List. For a brief description of the new interfaces, see Appendix C, Summary of Solaris 7 DDI/DKI Services.

Table F-2 Solaris 7 Common Access Functions

ddi_get8(9F)

reads 8 bits from device address 

ddi_get16(9F)

reads 16 bits from device address 

ddi_get32(9F)

reads 32 bits from device address 

ddi_get64(9F)

reads 64 bits from device address 

ddi_put8(9F)

writes 8 bits to device address 

ddi_put16(9F)

writes 16 bits to device address 

ddi_put32(9F)

writes 32 bits to device address 

ddi_put64(9F)

writes 64 bits to device address 

Modify Routines That Handle Data Sharing

If a device driver shares data structures with an application using ioctl(9E), devmap(9E), or mmap(9E), and the driver is recompiled for a 64-bit kernel but the application that uses the interface is a 32-bit program, the binary layout of data structures will be incompatible if they contain long types or pointers.

If a data structure is defined in terms of type long, but there is no actual need for 64-bit data items, the data structure should be changed to use fundamental types that remain 32 bits in LP64 (int and unsigned int) or the new fixed-width 32-bit types in <sys/inttypes.h>. In the remaining cases, where the data structures contain pointers or structure fields that need to be long (32-bits in an ILP32 kernel and 64-bits in an LP64 kernel), the driver needs to be aware of the different structure shapes for ILP32 and LP64 and determine whether there is a model mismatch between the application and the kernel.

To handle potential data model differences, the ioctl(9E), devmap(9E) and mmap(9E) driver entry points, which are passed arguments from user applications, need to be written to determine whether the argument came from an application using the same data type model as the kernel. The new DDI function ddi_model_convert_from(9F) enables drivers to determine this.

ddi_model_convert_from(9F)

This function takes the data type model of the user application as an argument and returns the following values:

DDI_MODEL_NONE is returned if no data conversion is necessary. This is the case when application and driver have the same data model (both are ILP32 or LP64). DDI_MODEL_ILP32 is returned if the driver is compiled to the LP64 data model and is communicating with a 32-bit application. Typically, the code that returns the application data model is conditionally compiled depending on the _MULTI_DATAMODEL macro. This macro is defined by the system when the driver supports multiple data models.

If the driver supports multiple data models, it will switch on the return value of ddi_model_convert_from(9F). The DDI_MODEL_ILP32 case should define a 32-bit version of the structure being passed in. Use ddi_copyin(9F) to copy the structure from user space to the 32-bit version of the structure, and then assign each field in the 32-bit structure to the 64-bit version. Otherwise, the code should be unchanged.

The sections that follow show code examples of the use of ddi_model_convert_from(9F).

ioctl(9E)

In a 32-bit system, the ioctl(9E) entry point takes an int as the argument to pass a 32-bit value or user address to the device driver. In a 64-bit system, this argument must handle 64-bit values and addresses. Therefore, the ioctl(9E) function prototype has changed from:

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

to:

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

Note that intptr_t arg remains 32-bits when compiled in the ILP32 kernel.

To determine whether there is a model mismatch between the application and the driver, the driver uses the FMODELS mask to determine the model type from the ioctl(9E) mode argument. The following values are passed in mode to identify the application data model:

The driver passes the data model type to ddi_model_convert_from(9F), which determines if adjustments are needed to the application data structures.

Example F-1 demonstrates the use of the _MULTI_DATAMODEL macro and the ddi_model_convert_from(9F) function.


Example F-1 ioctl(9E)

struct passargs {
	int		len;
	caddr_t		addr
} pa;

xxioctl(dev_t dev, int cmd, intptr_t arg, int mode, cred_t
*credp,
			int *rvalp)
{
	...
#ifdef _MULTI_DATAMODEL
	switch (ddi_model_convert_from(mode & FMODELS)) {
	case DDI_MODEL_ILP32:
	{
	  	struct passargs32 {
			int			len;
			uint32_t			*addr;
	  	} pa32;

		(void) ddi_copyin((void *)arg, &pa32,
					sizeof (struct passargs32), mode);
		pa.len = pa32.len;
		pa.addr = pa32.address;
		break;
	}
	case DDI_MODEL_NONE:
		(void) ddi_copyin((void *)arg, &pa,
					sizeof (struct passargs), mode);
		break;
	}
#else /* ! _MULTI_DATAMODEL */
	(void) ddi_copyin((void *)arg, &pa,
				sizeof (struct passargs), mode);
#endif /* ! _MULTI_DATAMODEL */
	do_ioctl(&pa);
	...
}

Data structure macros are another method of referring to the data structure from the user application. The macros effectively hide the difference between the data model of the user application and the driver. For more information see "Data Structure Macros".

devmap(9E)

To enable a 64-bit driver and a 32-bit application to share memory, the binary layout generated by the 64-bit driver must be the same as consumed by the 32-bit application.

To determine whether there is a model mismatch, devmap(9E) uses the model parameter to pass the data model type expected by the application. model is set to one of the following:

Example F-2 shows the devmap(9E) model parameter being passed to the ddi_model_convert_from(9F) function.


Example F-2 devmap(9E)

struct data {
	int			len;
	caddr_t			addr;
};

xxdevmap(dev_t dev, devmap_cookie_t dhp, offset_t offset,
			size_t len, size_t *maplen, uint_t model);
{
	struct data dtc;  /* local copy for clash resolution */
	struct data *dp = (struct data *)shared_area;

#ifdef _MULTI_DATAMODEL
	switch (ddi_model_convert_from(model)) {
	case DDI_MODEL_ILP32:
	{
		struct data32 {
			int			len;
			uint32_t			*addr;
		} *da32p;

		da32p = (struct data32 *)shared_area;
		dp = &dtc;
		dp->len = da32p->len;
		dp->address = da32p->address;
		break;
	}
	case DDI_MODEL_NONE:		
		break;
	}
#endif  /* _MULTI_DATAMODEL */
	/* continues along using dp */
	...
}

mmap(9E)

Because mmap(9E) does not have a parameter that can be used to pass data model information, the driver's mmap(9E) entry point should be written to use the new DDI function ddi_mmap_get_model(9F). This function returns one of the following values to indicate the application's data type model:

As with ioctl(9E) and devmap(9E), the model bits can be passed to ddi_model_convert_from(9F) to determine whether data conversion is necessary.

Example F-3 shows the use of ddi_mmap_get_model(9F).


Example F-3 mmap(9E)

struct data {
	int			len;
	caddr_t			addr
};

xxmmap(dev_t dev, off_t off, int prot)
{
	struct data dtc;  /* local copy for clash resolution */
	struct data *dp = (struct data *)shared_area;

#ifdef _MULTI_DATAMODEL
	switch (ddi_model_convert_from(ddi_mmap_get_model())) {
	case DDI_MODEL_ILP32:
	{
		struct data32 {
			int			len;
			uint32_t			*addr
		} *da32p;

		da32p = (struct data32 *)shared_area;
		dp = &dtc;
		dp->len = da32p->len;
		dp->address = da32p->address;
		break;
	}
	case DDI_MODEL_NONE:		
		break;
	}

#endif  /* _MULTI_DATAMODEL */
	/* continues along using dp */
	...
}

Check Changed Fields in DDI Data Structures

The data type of some of the fields in DDI data structures such as buf(9S) have been changed. Drivers that use these data structures should make sure that these fields are being used appropriately. The data structures and the fields that were changed in a significant way are listed below.

buf(9S)

size_t   b_bcount;      /* was type unsigned int */
size_t   b_resid;       /* was type unsigned int */
size_t   b_bufsize;      /* was type long */
The fields changed here pertain to transfer size, which can now exceed more than 4GB in future systems.

ddi_dma_attr(9S)

This structure defines attributes of the DMA engine and the device. Since these attributes specify register sizes, fixed-width data types have been used instead of fundamental types.

ddi_dma_cookie(9S)

uint32_t     dmac_address;    /* was type unsigned long */
size_t     dmac_size;     /* was type u_int */
This structure contains a 32-bit DMA address, so a fixed-width data type has been used to define it. The size has been redefined as size_t.

scsi_pkt(9S)

u_int		pkt_flags;			/* was type u_long */
int			pkt_time;			/* was type long */
ssize_t	pkt_resid;			/* was type long */
u_int		pkt_state;			/* was type u_long */
u_int		pkt_statistics;	/* was type u_long */
Since the flags, state and statistics fields do not need to grow they have been redefined as 32-bit integers. The data transfer size pkt_resid field does grow and has been redefined as ssize_t.

Check Changed Arguments of DDI Functions

Some of the DDI functions argument data types have been changed. These routines are listed below.

getrbuf(9F)

struct buf *getrbuf(int sleepflag);
In previous releases, sleepflag was defined as a type long.

drv_getparm(9F)

 int drv_getparm(unsigned int parm, void *value_p);
In previous releases, value_p was defined to be type unsigned long *.

In the 64-bit kernel, drv_getparm(9F) can be used to fetch quantities that are both 32-bit and 64-bit, yet the interface does not define the data types of these quantities which encourages simple programming errors.

The following new routines offer a safer alternative:

clock_t  ddi_get_lbolt(void);
time_t   ddi_get_time(void);
cred_t   *ddi_get_cred(void);
pid_t    ddi_get_pid(void);
Driver writers are strongly urged to use these routines instead of drv_getparm(9F).

delay(9F) and timeout(9F)

void delay(clock_t ticks);
timeout_id_t timeout(void (*func)(caddr_t), caddr_t arg, clock_t ticks);
The ticks argument to both of these routines has been changed from long to clock_t.

rmallocmap(9F) and rmallocmap_wait(9F)

struct map *rmallocmap(size_t mapsize);
struct map *rmallocmap_wait(size_t mapsize);
The mapsize argument to both of these routines has been changed from ulong_t to size_t.

scsi_alloc_consistent_buf(9F)

struct buf *scsi_alloc_consistent_buf(struct scsi_address *ap,
   struct buf *bp, size_t datalen, uint_t bflags,
   int (*callback )(caddr_t), caddr_t arg);
In previous releases, datalen was defined as an int and bflags was defined as a ulong.

uiomove(9F)

int uiomove(caddr_t address, size_t nbytes,
   enum uio_rw rwflag, uio_t *uio_p);
The nbytes argument was defined as a type long, but since it represents a size in bytes, size_t is more appropriate.

cv_timedwait(9F) and cv_timedwait_sig(9F)

 int cv_timedwait(kcondvar_t *cvp, kmutex_t *mp, clock_t timeout); 
int cv_timedwait_sig(kcondvar_t *cvp, kmutex_t *mp, clock_t timeout);
In previous releases, the timeout argument to both of these routines was defined to be of type long. Since they represent time in ticks, clock_t is more appropriate.

ddi_device_copy(9F)

 int ddi_device_copy(ddi_acc_handle_t src_handle,
   caddr_t src_addr, ssize_t src_advcnt,
   ddi_acc_handle_t dest_handle, caddr_t dest_addr,
   ssize_t dest_advcnt, size_t bytecount, uint_t dev_datasz);
The src_advcnt, dest_advcnt, dev_datasz arguments have changed type. These were previously defined as long, long and ulong_t respectively.

ddi_device_zero(9F)

int ddi_device_zero(ddi_acc_handle_t handle,
   caddr_t dev_addr, size_t bytecount, ssize_t dev_advcnt,
   uint_t dev_datasz):
In previous releases, dev_advcnt was defined as a type long and dev_datasz as a ulong_t.

ddi_dma_mem_alloc(9F)

int ddi_dma_mem_alloc(ddi_dma_handle_t handle,
   size_t length, ddi_device_acc_attr_t *accattrp,
   uint_t flags, int (*waitfp)(caddr_t), caddr_t arg,
   caddr_t *kaddrp, size_t *real_length,
   ddi_acc_handle_t *handlep);
In previous releases, length, flags and real_length were defined with types, uint_t, ulong_t and uint_t *.

Convert Well-Known Ioctl Interfaces

Many ioctl operations are common to the same class of device driver. For example, most disk drivers implement many of the dkio(7I) family of ioctls. Many of these interfaces copy in or copy out data structures from the kernel, and some of these data structures have changed size as a result of the LP64 data model. The following section lists the ioctls that now require explicit conversion in 64-bit driver ioctl routines for the dkio(7I), fdio(7I), fbio(7I),cdio(7I) and mtio(7I) families of ioctls.

Table F-3 Data Structures

ioctl Command 

Affected Data Structure 

Reference 

 DKIOCGAPARTstruct dk_map()dkio(7I)
 DKIOCSAPARTstruct dk_allmap()
 DKIOGVTOC struct partition()dkio(7I)
 DKIOSVTOCstruct vtoc()
 FBIOPUTCMAPstruct fbcmap()fbio(7I)
 FBIOPUTCMAPIstruct fbcmap_i()fbio(7I)
 FBIOGETCMAPI 
 FBIOSCURSORstruct fbcursor()fbio(7I)
 FBIOSCURSOR 
 CDROMREADMODE1struct cdrom_read()cdio(7I)
 CDROMREADMODE2 
 CDROMCDDAstruct cdrom_cdda()cdio(7I)
 CDROMCDXAstruct cdrom_cdxa()cdio(7I)
 CDROMSUBCODEstruct cdrom_subcode()cdio(7I)
 FDIOCMDstruct fd_cmd()fdio(7I)
 FDRAWstruct fd_raw()fdio(7I)
 MTIOCTOPstruct mtop()mtio(7I)
 MTIOCGETstruct mtget()mtio(7I)
 MTIOCGETDRIVETYPEstruct mtdrivetype_request()mtio(7I)
 USCSICMDstruct uscsi_cmd() Undocumented

Device Sizes

The nblocks property is exported by each slice of a block device drivers. It contains the number of 512 byte blocks that each slice of the device can support. The nblocks property is defined as a signed 32-bit quantity, which limits the maximum number of disk blocks to 1Tbyte.

Disk devices that provide more than 1Tbyte of storage per disk, must define the Nblocks property, which should still contain the number of 512 byte blocks that the device can support. However, Nblocks is a signed 64-bit quantity, which removes any practical limit on disk space.

The nblocks property is now deprecated; all disk devices should provide the Nblocks property.