***************************************************************************** * MAKING AN I/O DRIVER FOR A SENSOR DEVICE * * * * * * Copyright (c) 2002, 2003 Daniel P. Bovet, Marco Cesati, * * and Vincenzo Garofalo * * Permission is granted to copy, distribute and/or modify this document * * under the terms of the GNU Free Documentation License, Version 1.1, * * published by the Free Software Foundation; with no Invariant Sections, * * with no Front-Cover Texts, and with no Back-Cover Texts. A copy of the * * license is included in the file named LICENSE. * * * * (version 2.0) * ***************************************************************************** The objective is to illustrate how to make a simple interrupt-driven I/O driver for a new device called "lp_sensor". Contrary to the led device (see "MAKING AN I/O DRIVER FOR A LED DEVICE"), the lp_sensor device makes use of a temperature sensor and its response time is non negligible, thus justifying the use of interrupts to signal the end of temperature readings. The I/O device that we shall baptize as "lp_sensoe" has already been described in the slides. As a general rule, the linux-2.4.18kh patched kernel will include all I/O drivers covered so far. The patch-2.4.18kh2 patch applied to linux-2.4.18kh will yield an updated version of linux-2.4.18kh that includes two new I/O drivers, namely those for the lp_led and for the lp_sensor devices. This is the second driver we introduce, thus we shall show in this lecture how to add a lp_sensor I/O driver to a kernel that already includes a lp_led I/O driver. In this way, we'll avoid describing a series of steps that have been already performed in a previous patch. More explicitly, we assume we are acting on a patched kernel that has the following features: 1) a kh EXTRAVERSION in its main Makefile 2) a new_iodrivers main directory that already contains the lp_led I/O driver code 3) a main Makefile that requires the compilation of all programms in the new_iodrivers main directory The project consists of three parts: a) create a new device file called lp_sensor and assign an unused major number to the new device b) define open(), close(), and read() file operations for the new device and register the new character device c) test the I/O driver by running a test program that reads from the lp_sensor I/O device ***************************************************************************** STEP 1: modify the linux/arch/i386/config.in file ***************************************************************************** add the following line in the 'Kernel hacking' main menu: tristate 'I/O driver for lp_sensor' CONFIG_LP_SENSOR ***************************************************************************** STEP 2: Add a new kernel hacking configuration option ***************************************************************************** add in linux/Documentation/Configure.help the following lines right after CONFIG_MAGIC_SYSRQ: Support for lp_sensor device CONFIG_LP_SENSOR If you say Y here, you will have an I/O driver capable of handling the lp_sensor device connected to the parallel port. ***************************************************************************** STEP 3: Reserve a major number for the new lp_sensor I/O device ***************************************************************************** Since both the lp_led and the lp_sensor devices are connected to the parallel port, we can assign the same major number, namely 42, to the lp_sensor device file. We thus add the following lines in the include/linux/major.h file: #define LP_SENSOR_MAJOR 42 ***************************************************************************** STEP 4: Define a new lp_sensor char device file with major number 42 in the /dev directory ***************************************************************************** issue as superuser the following command: mknod /dev/lp_sensor c 42 0 ***************************************************************************** STEP 5: Modify the Makefile of the linux/new_iodrivers directory ***************************************************************************** Add right after ifeq ($(CONFIG_LP_LED),y) obj-y += lp_led.o else ifeq ($(CONFIG_LP_LED),m) obj-m += lp_led.o endif endif the following lines: ifeq ($(CONFIG_LP_SENSOR),y) obj-y += lp_sensor.o else ifeq ($(CONFIG_LP_SENSOR),m) obj-m += lp_sensor.o endif endif ***************************************************************************** STEP 6: Add the lp_sensor.c file in the new_iodrivers directory ***************************************************************************** #include #include #include #include #include #include #include #include #include /* Addresses of the parallel port */ #define PData 0x378 #define PStatus 0x379 #define PControl 0x37a /* IRQ number of the parallel port */ #define IRQ 7 int verbose = 0; /* the device driver descriptor */ static struct { struct semaphore sem; wait_queue_head_t wait; unsigned char data; unsigned long long tsc; } foo_dev; /* forward declarations */ static ssize_t lp_sensor_read(struct file *, char *, size_t, loff_t *); static int lp_sensor_open(struct inode *, struct file *); static int lp_sensor_close(struct inode *, struct file *); static void lp_sensor_interrupt(int, void *, struct pt_regs *); /* the methods of the character device */ static struct file_operations lp_sensor_fops = { owner: THIS_MODULE, read: lp_sensor_read, open: lp_sensor_open, release: lp_sensor_close, }; /* initializer of the character device */ int lp_sensor_init(void) { int cc; cc = register_chrdev(LP_SENSOR_MAJOR, "lp_sensor_card", &lp_sensor_fops); if (cc < 0) { printk(KERN_INFO "Cannot register the lp_sensor device\n"); return 1; } cc = request_irq(IRQ, lp_sensor_interrupt, SA_INTERRUPT, "lp_sensor", NULL); if (cc < 0) { printk(KERN_INFO "Cannot get the irq line %d for lp_sensor device\n", IRQ); unregister_chrdev(LP_SENSOR_MAJOR, "lp_sensor_card"); return 1; } init_MUTEX(&foo_dev.sem); init_waitqueue_head(&foo_dev.wait); return 0; } /* de-initializer of the character device */ void lp_sensor_exit(void) { int cc; free_irq(IRQ, NULL); cc = unregister_chrdev(LP_SENSOR_MAJOR, "lp_sensor_card"); if (cc < 0) printk(KERN_INFO "Cannot unregister lp_sensor device\n"); } /* the open method of the character device */ int lp_sensor_open(struct inode *inode, struct file *file) { outb_p(0x00, PData); outb_p(0x00, PStatus); outb_p(0x24, PControl); outb_p(0x34, PControl); return 0; } /* the close method of the character device */ int lp_sensor_close(struct inode *inode, struct file *file) { outb_p(0x0b, PControl); outb_p(0x00, PData); outb_p(0x00, PStatus); return 0; } /* the read method of the character device */ ssize_t lp_sensor_read(struct file *file, char *buf, size_t count, loff_t *ppos) { wait_queue_t wait; unsigned long long tsc; if (down_interruptible(&foo_dev.sem)) return -ERESTARTSYS; init_waitqueue_entry(&wait, current); add_wait_queue(&foo_dev.wait, &wait); rdtscll(tsc); /* It's crucial to declare the process TASK_INTERRUPTIBLE !!!before!!! issuing the outb instructions and not after. Otherwise, if the device issues an interrupt right after the third outb and before set_current_state(TASK_INTERRUPTIBLE), the process resumes execution after the interrupt handler terminates, becomes TASK_INTERRUPTIBLE and stops forever inside schedule because the event it waits for already occurred. */ set_current_state(TASK_INTERRUPTIBLE); outb_p(0x34,PControl); outb_p(0x3c,PControl); outb_p(0x34,PControl); schedule(); set_current_state(TASK_RUNNING); remove_wait_queue(&foo_dev.wait, &wait); if (put_user(foo_dev.data, buf)) return -EFAULT; up(&foo_dev.sem); if (verbose) printk(KERN_INFO "Delay of interrupt: %llu bus cycles\n", foo_dev.tsc-tsc); return 1; } /* the interrupt handler */ static void lp_sensor_interrupt(int irq, void *dev_id, struct pt_regs *regs) { unsigned long long t; unsigned flags; /* read the Time Stamp Counter */ rdtscll(t); foo_dev.tsc = t; /* access the device's hardware */ save_flags(flags); __cli(); outb_p(0x34,PControl); outb_p(0x30,PControl); foo_dev.data = inb_p(PData); outb_p(0x34,PControl); restore_flags(flags); /* wake up the process sleeping in the device wait queue */ wake_up_interruptible(&foo_dev.wait); } EXPORT_NO_SYMBOLS; /* newer linux versions module_init(lp_sensor_init); module_exit(lp_sensor_exit); */ int init_module(void) { printk(KERN_INFO "Loading the lp_sensor driver\n"); return lp_sensor_init(); } void cleanup_module(void) { printk(KERN_INFO "Unloading the lp_sensor device\n"); lp_sensor_exit(); } MODULE_AUTHOR("D.P. Bovet, M. Cesati, and V. Garofalo"); MODULE_DESCRIPTION("LKHC driver for lp_sensor character device"); MODULE_LICENSE("GPL"); MODULE_PARM(verbose, "i"); ***************************************************************************** STEP 7: Test the I/O driver module: ***************************************************************************** a) run make menuconfig and set to 'm' the CONFIG_LP_SENSOR kernel hackers option b) run make dep, recompile the kernel and copy the arch/i386/boot/bzImage in /boot/vmlinuz-2.4.18kh or on a floppy c) run make modules and make modules_install d) run lilo and reboot the system and select linux-2.4.18kh as the system to boot e) login as root and insert the module by issuing the command: insmod /usr/src/linux/new_iodrivers/lp_sensor.o or: modprobe lp_sensor (if insmod works but modprobe doesn't, this might be due to the fact that you are using outdated libraries for handling modules) f) test whether it has been loaded correctly by typing: lsmod lp_sensor g) run the following Tlp_sensor test program: /* Tlp_sensor.c test program */ #include #include #include #include #include #include #include int main (void) { char c; int cc, fd; unsigned char buf; fd = open("/dev/lp_sensor",O_RDONLY); if (fd < 0) { perror("/dev/lp_sensor"); return 1; } for(;;) { printf("\nSelect an option:\n\n"); printf("read temperature: t\n\n"); printf("quit: q\n\n"); c=getchar(); if (c == 't') { cc = read(fd, &buf, 1); printf ("\nTemperature: 0x%x\n",(int)buf); c = getchar(); } else if (c == 'q') break; } cc = close(fd); return 0; }