Writing Device Drivers

C Language and Compiler Modes

The Sun WorkShopTM Compiler C version 4.2 provides ANSI C compilers for the Solaris environment. It supports several compilation modes, a number of useful keywords, and function prototypes.

Compiler Modes

Note the following compiler modes.

-Xa (ANSI C Mode)

This mode accepts ANSI C and Sun C compatibility extensions. In case of a conflict between ANSI and Sun C, the compiler issues a warning and uses ANSI C interpretations. This is the default mode.

-Xt (Transition Mode)

This mode accepts ANSI C and Sun C compatibility extensions. In case of a conflict between ANSI and Sun C, a warning is issued and Sun C semantics are used.

Function Prototypes

Function prototypes specify the following information to the compiler:


Example 3-5 Function Prototypes

static int
xxgetinfo(dev_info_t *dip, ddi_info_cmd_t cmd, void *arg,
   void **result)
{
	/* definition */
}
static int
xxopen(dev_t *devp, int flag, int otyp, cred_t *credp)
{
	/* definition */
}

This allows the compiler to do more type checking and also to promote the types of the parameters to the type expected by the function. For example, if the compiler knows that a function takes a pointer, casting NULL to that pointer type is no longer necessary. Prototypes are provided for all Solaris 7 DDI/DKI functions, provided the driver includes the proper header file (documented in the manual page for the function).

Keywords

ANSI C provides the following driver-related keywords.

const

The const keyword can be used to define constants instead of using #define:

	const int			count=5;

However, it is most useful when combined with function prototypes. Routines that should not be modifying parameters can define the parameters as constants, and the compiler will then give errors if the parameter is modified. Because C passes parameters by value, most parameters don't need to be declared as constants. If the parameter is a pointer, though, it can be declared to point to a constant object:

	int strlen(const char *s)
 	{
 		...
 	}

Any attempt to change the string by strlen() is an error, and the compiler will catch the error.

volatile

The correct use of volatile is necessary to prevent elusive bugs. It instructs the compiler to use exact semantics for the declared objects--in particular, to not optimize away or reorder accesses to the object. There are two instances where device drivers must use the volatile qualifier:

  1. When data refers to an external hardware device register (memory that has side effects other than just storage). Note, however, that if the DDI data access functions are used to access device registers, it is not necessary to use volatile.

  2. When data refers to global memory that is accessible by more than one thread, is not protected by locks, and therefore is relying on the sequencing of memory accesses

    In general, drivers should not qualify a variable as volatile if it is merely accessible by more than one thread and protected from conflicting access by synchronization routines.

    The following example uses volatile. A busy flag is used to prevent a thread from continuing while the device is busy and the flag is not protected by a lock:

    	while (busy) {
      		/* do something else */
      	}

    The testing thread will continue when another thread turns off the busy flag:

    	busy = 0;

    However, since busy is accessed frequently in the testing thread, the compiler may optimize the test by placing the value of busy in a register, then test the contents of the register without reading the value of busy in memory before every test. The testing thread would never see busy change and the other thread would only change the value of busy in memory, resulting in deadlock. Declaring the busy flag as volatile forces its value to be read before each test.


    Note -

    It would probably be preferable to use a condition variable mutex, discussed under "Condition Variables" rather than the busy flag in this example.


    It is also recommended that the volatile qualifier be used in such a way as to avoid the risk of accidental omission. For example, this code

    	struct device_reg {
     		volatile uint8_t csr;
     		volatile uint8_t data;
     	};
     	struct device_reg *regp;

    is recommended over:

    	struct device_reg {
     		uint8_t csr;
     		uint8_t data;
     	};
     	volatile struct device_reg *regp;

    Although the two examples are functionally equivalent, the second one requires the writer to ensure that volatile is used in every declaration of type struct device_reg. The first example results in the data being treated as volatile in all declarations and is therefore preferred. Note as mentioned above, that the use of the DDI data access functions to access device registers makes it unnecessary to qualify variables as volatile.