Writing Linux Device Drivers

by

Ajith Kumar B.P., Nuclear Science Centre, New Delhi 110 067

This document is for the impatient ones who want to write code for communicating to some hardware device without going into the details of Linux kernel internals. The objective is to get you started at the earliest. I started programming hardware under DOS. It was very easy for simple tasks; you can access the Input/output ports from anywhere in the program. There is only one program running at a time and that is in the Real mode, having permissions to execute any CPU instruction. I found DOS messy while handling interrupts and DMA.

Under a multi-user OS like Linux things are slightly different. The CPU supports different run levels allowing the operating system to exercise control over the user programs. Application programs run in the user mode where they don't have permission to access the hardware directly. To access hardware devices, you make System Calls. Programs that are part of the OS, running in the kernel mode handle the system calls.

Our objective is to read, write and control some piece of hardware interfaced to the PC. This is done by executing read/write instruction to the I/O port addresses where the device is connected. We could do this directly under DOS. Under Linux we have the extra trouble of putting these instructions in a device driver module and calling it from our application program. The advantages offered far more outweigh this little extra work.

Hardware Required

We need some hardware to try our driver code. We will use the Printer Port for this purpose. In the IBM PC architecture, the printer port is accessed through three bytes in the I/O address space (mostly starting from 378 hex). Our example driver will do use these addresses. Those who can do a bit of soldering can do the following

The GFN Driver

We will start with an example driver that will read and write the printer ports of a PC, as name implies it is Good For Nothing. Linux has three classes of device drivers; character devices, block devices and network interfaces. We are writing character device drivers. Remember that the code is not very good or portable. There are very good books to learn how to write good device drivers.

Here is the driver code gfn.c

In the above driver, the only routines doing our actual work is gfn_read & gfn_write. The rest of it is the Device Driver framework. Let us examine the driver functions;

init_module(void) & cleanup_module()

Linux supports loadable modules. The compiled code is added to the kernel by using the program 'insmod'. The init_module() routine is executed while loading the module. Similarly 'rmmod' is used for removing the module. cleanup_module is executed while removing the module. The kernel functions register_chrdev is called from init_module with a Device Major Number. Each device should have a unique Major Number.

gfn_open() & gfn_close()

Under Unix all the devices appear as special files. Files in the /dev directory are associated with different devices. We also need to create a special file in the /dev directory with the proper Major and Minor numbers inorder to call the GFN driver from a user program. To write/read a file you need to open it first. When the user program executes the system call open() on this driver, kernel will execute 'gfn_open'. When you close the file, the driver routine 'gfn_close' will be executed. Remember that more than one user program can use the driver at a time. We will see more of it later.

gfn_write()

Data from user space is taken and written to the Data Port of the printer. We use the printk() function displays the value received by the driver from the user space. This function is essential during debugging. The kernel routine copy_from_user() is required to transfer the data from user's area.

gfn_read()

This routine reads the printer status port and puts the data into the user's area using copy_to_user(). The normal practice is to get status information using the ioctl function call, explained later. Here we are doing so to monitor the changes by putting the printer on and off, we have no data to read either. This is an unusually simple version of read. Data is ready all the time. Remember that if you are reading from a keyboard, your program is blocked until you press a key. How it is done will be explained later.

Compiling and Loading GFN

The modules should be compiled with some extra options as shown below. The optimzation option is required. It is better to turn all warnings on.

# cc -Wall -O2 -c -DMODULE -D__KERNEL__ gfn.c

If everything is fine, the object module 'gfn.o' will be written to disk. Load it using 'insmod' and see the list of loaded modules by using 'lsmod'

#insmod gfn.o

#lsmod


Now we need a user program gfntes.c to test the driver code. The test program opens the file '/dev/gfn' for reding and writing. We need to create this special file by the command

#mknod /dev/gfn c 60 0

This will create a character type file with Major number sixty and Minor number zero. Set the permissions using,

#chmod 666 /dev/gfn

Compile gfntes.c and execute it

#cc -o gfntes gfntes.c

#./gfntes

This is to be done from a text console to see the outputs from the printk statements inside the driver code. You may NOT see them from 'xterm' under X-Window.if you have a printer connected to the port, run the program under ON and OFF conditions to see the difference in status output. Write values to set different bits of the control port and see what happens to the printer.

The ioctl system call

In addition to reading and writing the device, we need to perform various types of hardware control via the device driver. Control operations are usually supported through the ioctl method. ( In the last example we used read() for getting status information, only to test those routines.) Now let us add an ioctl call to the driver.

The following lines are added to gfn.c to make the new driver gfn2.c.

// The ioctl commads

#define GETSTATUS 1000

#define SETCONPORT 1001

int gfn_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg)

{

u8 data;

switch(cmd) {

case GETSTATUS:

data = inb(STATPORT);

printk("IOCTL GETSTATUS: to user %x hex\n",data);

if(copy_to_user((u8 *) arg, &data, 1)) return -EFAULT;

break;

case SETCONPORT:

if(copy_from_user(&data, (u8 *) arg, 1)) return -EFAULT;

printk("IOCTL SETCONPORT: from user %x hex\n",data);

outb(data, CONPORT);

break;

}


The third argument is the command sent by the user program. The last argument 'arg' is a pointer, used for transferring any amount of data between the user program and the driver. The user program and driver should agree upon the format of this data. Test the new driver gfn2.c using the test program gfntes2.c listed below. Note that we are still opening the driver by accessing '/dev/gfn' . This is okey since we are using the same major number, 60, in driver gfn2 also. You can load only one of them at a time using the same Major Number. Remove the existing driver by 'rmmod', otherwise you will get the following error from insmod;

gfn2.o: init_module: Input/output error

Hint: insmod errors can be caused by incorrect module parameters, including invalid IO or IRQ parameters

Compile the test program gfntes2.c and run it.

GFN falling asleep

In a multi-tasking system you may have large number of processes running at a time. All the processes are given time slices one after other in a circular queue. What really save the system is the processes that are waiting for some events do not consume CPU time. They don't get a chance to run until that event occur. This is done in the device driver. For example, if your program tries to read from the keyboard, it will be blocked until the user press some key. The process is put to sleep by the driver. Then an external event should come and the interrupt service routine will wakeup the process, ie. mark it as runnable. The next time scheduler will give it a chance to run. Since we don't have a hardware device to generate an external event, we will use the write system call to do the same. Then we can write two user programs. One will go to sleep by calling read and the other will wake it up using a write system call. We can also do the same by using an ioctl call.

Modify the file gfn.c by changing the read() & write() routine as shown below. Also declare a wait queue to sleep. The modified file is gfn3.c. It can be tested using stes.c andwtes.c from two different terminal windows. Sltes will block on the driver and wtes will wake it.


static DECLARE_WAIT_QUEUE_HEAD(my_wait_queue);

static ssize_t gfn_read(struct file *file, char *buffer, size_t count, loff_t *

{

u8 data;

printk("Process put to Sleep in READ\n");

interruptible_sleep_on(&my_wait_queue);

printk("Woken up. data = %x hex\n", data);

data = inb(STATPORT); // read the printer status port

// transfer data from kernel address space to user address space

if(copy_to_user( buffer, &data, sizeof(u8))) return -EFAULT;

return 0;

}


Once the process sleeps, the system call will not return until the process is wokenup somehow. This is supposed to be done by interrupt handling routines. Before going in to that we will do a trick ( A useless one in real life) to wake the process. Modify the write function to do that as shown below.

static ssize_t gfn_write(struct file *file, const char *buf, size_t count, loff_t *ppos)

{

u8 data;

printk("WRITE: Waking up the process Put to sleep by READ\n");

wake_up_interruptible(&my_wait_queue);

if (copy_from_user(&data, buf, sizeof(data))) return -EFAULT;

printk("Data from user %x hex\n", data);

outb(data, CONPORT);

return 0;

}

Interrupt handling

Interrupts are asynchronous events triggered by some external event, like pressing a key on a keyboard. The PC has several interrupts lines connected to various devices. Read the IBM PC data book for details. The printer port uses Interrupt number 7. Our driver will register with the kernel so that our interrupt handler will be called by the kernel when an interrupt strikes. When you should acquire the interrupt depends on your needs. The code below does it while loading the module and keeps it until the module is unloaded. You can do the same during open and close. In that case a second process trying to open the driver will fail. The kernel functions request-irq() and free_irq() are used.

The modified driver is gfn4.c.

The irqtes.c is used for testing the interrupts. Run irqtes and it will wait for an interrupt. You can generate the interrupt by opening the switch connected between pins 9 and 10 of the parallel port. It seems like you can trigger an interrupt by putting the printer OFF and ON.


references

1. Linux Device Drivers by Allessandro Rubini, O'really publishers