Writing Portable Device Drivers

Follow the kernel team's rules to make your drivers work on all architectures.
Endian Issues

Processors store internal data in one of two ways: little-endian or big-endian. Little-endian processors store data with the right-most bytes (those with a higher address value) being the most significant, while big-endian processors store data with the left-most bytes (those with a lower address value) being the most significant.

For example, Table 2 shows how the decimal value 684686 is stored in a 4-byte integer on the two different processor types (684686 decimal = a72be hex = 00000000 00001010 01110010 10001110 binary).

Table 2. How the Decimal Value 684686 is Stored in a 4-Byte Integer

Intel processors, for example the i386 and IA-64 series, are little-endian machines, whereas the SPARC processors are big-endian. The PowerPC processors can be run in either little- or big-endian mode, but for Linux, they are defined as running in big-endian mode. The ARM processor can be either, depending on the specific ARM chip being used, but usually it also runs in big-endian mode.

Because of the different endian types of processors, you need to be aware of data you receive from external sources and the order in which it appears. For example, the USB specification dictates that all multibyte data fields are in little-endian form. So if you have a USB driver that reads a multibyte field from the USB connection, you need to convert that data into the processor's native format. Code that assumes the processor is little-endian could ignore the data format coming from the USB connection successfully. But this same code would not work on PowerPC or ARM processors and is the leading cause of drivers that are broken on different platforms.

Thankfully, there are a number of helpful macros that have been created to make this an easy task. All of the following macros can be found in the asm/byteorder.h header file.

To convert from the processor's native format into little-endian form you can use the following functions:

u64 cpu_to_le64 (u64);
u32 cpu_to_le32 (u32);
u16 cpu_to_le16 (u16);

To convert from little-endian format into the processor's native format you should use these functions:

u64 le64_to_cpu (u64);
u32 le32_to_cpu (u32);
u16 le16_to_cpu (u16);
For big-endian forms, the following functions are available:
u64 cpu_to_be64 (u64);
u32 cpu_to_be32 (u32);
u16 cpu_to_be16 (u16);
u64 be64_to_cpu (u64);
u32 be32_to_cpu (u32);
u16 be16_to_cpu (u16);
If you have a pointer to the value to convert, then you should use the following functions:
u64 cpu_to_le64p (u64 *);
u32 cpu_to_le32p (u32 *);
u16 cpu_to_le16p (u16 *);
u64 le64_to_cpup (u64 *);
u32 le32_to_cpup (u32 *);
u16 le16_to_cpup (u16 *);
u64 cpu_to_be64p (u64 *);
u32 cpu_to_be32p (u32 *);
u16 cpu_to_be16p (u16 *);
u64 be64_to_cpup (u64 *);
u32 be32_to_cpup (u32 *);
u16 be16_to_cpup (u16 *);
If you want to convert the value within a variable and store the modified value in the same variable (in situ), then you should use the following functions:
void cpu_to_le64s (u64 *);
void cpu_to_le32s (u32 *);
void cpu_to_le16s (u16 *);
void le64_to_cpus (u64 *);
void le32_to_cpus (u32 *);
void le16_to_cpus (u16 *);
void cpu_to_be64s (u64 *);
void cpu_to_be32s (u32 *);
void cpu_to_be16s (u16 *);
void be64_to_cpus (u64 *);
void be32_to_cpus (u32 *);
void be16_to_cpus (u16 *);
As stated before, the USB protocol is in little-endian format. The code snippet from drivers/usb/serial/visor.c presented in Listing 2 shows how a structure is read from the USB connection and then converted into the proper CPU format.

Listing 2. How a structure is read from the USB connection and converted into the proper CPU format.

Data Alignment

The gcc compiler typically aligns individual fields of a structure on whatever byte boundary it likes in order to provide faster execution. For example, consider the code and resulting output shown in Listing 3.

Listing 3. Alignment of Individual Fields of a Structure

The output shows that the compiler aligned fields b and c in the struct foo on even byte boundaries. This is not a good thing when we want to overlay a structure on top of a memory location. Typically driver data structures do not have even byte padding for the individual fields. Because of this, the gcc attribute (packed) is used to tell the compiler not to place any ``memory holes'' within a structure.

If we change the struct foo structure to use the packed attribute like this:

struct foo {
    char    a;
    short   b;
    int     c;
} __attribute__ ((packed));

Then the output of the program changes to:

offset A = 0
offset B = 1
offset C = 3
Now there are no more memory holes in the structure.

This packed attribute can be used to pack an entire structure, as shown above, or it can be used only to pack a number of specific fields within a structure.

For example, the struct usb_ctrlrequest is defined in include/usb.h as the following:

struct usb_ctrlrequest {
    __u8 bRequestType;
    __u8 bRequest;
    __u16 wValue;
    __u16 wIndex;
    __u16 wLength;
} __attribute__ ((packed));

This ensures that the entire structure is packed, so that it can be used to write data directly to a USB connection.

But the definition of the struct usb_endpoint_descriptor looks like:

struct usb_endpoint_descriptor {
    __u8  bLength           __attribute__ ((packed));
    __u8  bDescriptorType   __attribute__ ((packed));
    __u8  bEndpointAddress  __attribute__ ((packed));
    __u8  bmAttributes      __attribute__ ((packed));
    __u16 wMaxPacketSize    __attribute__ ((packed));
    __u8  bInterval         __attribute__ ((packed));
    __u8  bRefresh          __attribute__ ((packed));
    __u8  bSynchAddress     __attribute__ ((packed));
    unsigned char *extra;   /* Extra descriptors */
    int extralen;

This ensures that the first part of the structure is packed and can be used to read directly from a USB connection, but the extra and extralen fields of the structure can be aligned to whatever the compiler thinks will be fastest to access.



Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

re: interact wit pci using linux

sup's picture

i work on linux ver 2.2, i would like to interact with the pci bus and pci devices what apis and structures should i use and in which header file should i include for its proper functioning.

thanks and regards,


Why all the kernel drivers

Don's picture

This is a fine article on kernel drivers, but why write all these things as kernel drivers? The I2C interface (and the USB interface) both provide for access to devices from user-space. For many devices, this is all that is needed. Yes for file systems and network, etc. a kernel driver is needed, but why clutter up the kernel for every gadget that comes along?

It's a must read document

Bhupesh's picture

It's a must read document for beginners :)

Re: Writing Portable Device Drivers

Anonymous's picture

It's really a helpful article !

Re: Writing Portable Device Drivers

Anonymous's picture

Its a good artical for newbies entering into driver deveopment.
The artical is very cear and understanding.


Re: Writing Portable Device Drivers

Anonymous's picture

this is a good article specific to writing efficient and portable Linux device driver. It has cleared some of my confusion as well as raised a few.

I appreciate it.

nikhil bhargava

Hi,i wrote a device driver

anonymous's picture


i wrote a device driver for USB for FC6. after writing the code and compiling it, how could i make it start when the usb is plugged in? should i also use insmod and rmmod?