***************************************************************************** * MAKING A MODULE INTERRUPT DRIVEN I/O DRIVER * * * * * * Copyright (c) 2001 Daniel P. Bovet, Marco Cesati, Cosimo Comella 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 1.0) * ***************************************************************************** We shall use a new I/O device connected to the parallel port that includes a sensor that reads the temperature after some delay. Interrupt driven drivers are preferrable for such kinds of devices. The circuit called lp_temp is shown in a figure outside of this file. The I/O ports of the parallel port are used in a similar way as for the lp_led driver, except that now we use the interrupt enable/disable flag. ***************************************************************************** STEP 0: set the proper EXTRAVERSION value in Makefile ***************************************************************************** replace: EXTRAVERSION = with: EXTRAVERSION = kh11 ***************************************************************************** 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_temp' CONFIG_LP_TEMP ***************************************************************************** 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_temp device CONFIG_LP_TEMP If you say Y here, you will have an I/O driver capable of handling the lp_temp device connected to the parallel port. **************************************************************************** STEP 3: create a new directory linux/new_iodrivers that will contain the new lp_led.o object file of the I/O driver routines ***************************************************************************** mkdir new_iodrivers ***************************************************************************** STEP 4: modify the main linux/Makefile ***************************************************************************** a) replace CORE_FILES =kernel/kernel.o mm/mm.o fs/fs.o ipc/ipc.o with: CORE_FILES =kernel/kernel.o mm/mm.o fs/fs.o ipc/ipc.o new_iodrivers.o b) replace: SUBDIRS =kernel drivers mm fs net ipc lib with: SUBDIRS =kernel drivers mm fs net ipc lib new_iodrivers ***************************************************************************** STEP 5: Reserve a major number for the new lp_led I/O device ***************************************************************************** Finding a stable free major number is not easy. Do not use high numbers such as 254 or similar because they may be used before our device is registered, when performing dynamic assignment of free major numbers. We use 42 described as follows in the linux/Documentation/devices.txt file: 42 Demo/sample use This number is intended for use in sample code, as well as a general "example" device number. It should never be used for a device driver that is being distributed; either obtain an official number or use the local/experimental range. The sudden addition or removal of a driver with this number should not cause ill effects to the system (bugs excepted.) We thus add the following lines in the include/linux/major.h file: #define LP_TEMP_MAJOR 42 ***************************************************************************** STEP 6: Define a new lp_temp char device file with major number 42 in the /dev directory ***************************************************************************** issue as superuser the following command: mknod /dev/lp_temp c 42 0 ***************************************************************************** STEP 7: Create the Makefile for the linux/new_iodrivers directory ***************************************************************************** # We only get in/to here if CONFIG_LP_TEMP = 'y' or 'm' O_TARGET := new_iodrivers.o obj-m := obj-y := ifeq ($(CONFIG_LP_TEMP),y) obj-y += lp_temp.o else ifeq ($(CONFIG_LP_TEMP),m) obj-m += lp_temp.o endif endif include $(TOPDIR)/Rules.make ***************************************************************************** STEP 8: Add the lp_temp.c file in the new_IOdrivers directory ***************************************************************************** #include #include #include #include #include #include #include #include #include /* I/O addresses of the parallel port */ #define BASE 0x378 #define STAT (BASE+1) #define DATA (BASE+2) /* BASE -> Data port D7 D6 D5 D4 D3 D2 D1 D0 STAT -> Status port Busy Ack PaperOut SelectIn Error IRQ - - DATA -> Control port - - Bidir enIRQ SelPrint Reset LF Strob */ /* 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_temp_read(struct file *, char *, size_t, loff_t *); static int lp_temp_open(struct inode *, struct file *); static int lp_temp_close(struct inode *, struct file *); static void lp_temp_interrupt(int, void *, struct pt_regs *); /* the methods of the character device */ static struct file_operations lp_temp_fops = { owner: THIS_MODULE, read: lp_temp_read, open: lp_temp_open, release: lp_temp_close, }; /* initializer of the character device */ int lp_temp_init(void) { int cc; cc = register_chrdev(LP_TEMP_MAJOR, "lp_temp_card", &lp_temp_fops); if (cc < 0) { printk(KERN_INFO "Cannot register the lp_temp device\n"); return 1; } cc = request_irq(IRQ, lp_temp_interrupt, SA_INTERRUPT, "lp_temp", NULL); if (cc < 0) { printk(KERN_INFO "Cannot get the irq line %d for lp_temp device\n", IRQ); return 1; } init_MUTEX(&foo_dev.sem); init_waitqueue_head(&foo_dev.wait); return 0; } /* de-initializer of the character device */ void lp_temp_exit(void) { int cc; free_irq(IRQ, NULL); cc = unregister_chrdev(LP_TEMP_MAJOR, "lp_temp_card"); if (cc < 0) printk(KERN_INFO "Cannot unregister lp_temp device\n"); } /* the open method of the character device */ int lp_temp_open(struct inode *inode, struct file *file) { MOD_INC_USE_COUNT; outb_p(0x00, BASE); outb_p(0x00, STAT); outb_p(0x24, DATA); outb_p(0x34, DATA); return 0; } /* the close method of the character device */ int lp_temp_close(struct inode *inode, struct file *file) { MOD_DEC_USE_COUNT; outb_p(0x0b, DATA); outb_p(0x00, BASE); outb_p(0x00, STAT); return 0; } /* the read method of the character device */ ssize_t lp_temp_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,DATA); outb_p(0x3c,DATA); outb_p(0x34,DATA); 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_temp_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,DATA); outb_p(0x30,DATA); foo_dev.data = inb_p(BASE); outb_p(0x34,DATA); 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_temp_init); module_exit(lp_temp_exit); */ int init_module(void) { printk(KERN_INFO "Loading the lp_temp driver\n"); return lp_temp_init(); } void cleanup_module(void) { printk(KERN_INFO "Unloading the lp_temp device\n"); lp_temp_exit(); } MODULE_AUTHOR("D.P. Bovet, M. Cesati, and V. Garofalo"); MODULE_DESCRIPTION("LKHC driver for lp_temp character device"); MODULE_LICENSE("GPL"); MODULE_PARM(verbose, "i"); ***************************************************************************** STEP 9: Test the I/O driver module: ***************************************************************************** a) run make menuconfig and set to 'm' the CONFIG_LP_TEMP kernel hackers option b) run make dep, recompile the kernel and copy the arch/i386/boot/bzImage in /boot/vmlinuz-2.4.16kh11 or on a floppy c) run make modules and make modules_install d) run lilo and reboot the system and select linux-2.4.16kh11 as the system to boot e) login as root and insert the module: insmod new_iodrivers/lp_led.o f) test whether it has been loaded correctly by typing: lsmod lp_led g) run the following Tlp_temp test program: /* Tlp_temp.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_temp",O_RDONLY); if (fd < 0) { perror("/dev/lp_temp"); 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; } ***************************************************************************** * HOW TO PROGRAM THE PARALLEL PORT * * * ***************************************************************************** The parallel port uses a 25-pin connector. Pins 18-25 are used as ground. The remaining pins can be used in different ways. Pins 1-9, 14, 16 and 17 are available without restriction for output to the device. Pins 1-17 are available without restriction for fetching signals from the device. It is an asynchronous port, bit 0 is represented as a 0V signal while bit 1 is represented as a 5V signal (on laptops, 3.5V signals are used). IBM-compatible PCs usually have two distinct parallel ports. Linux calls the corresponding device files /dev/lp0 and /dev/lp1. The I/O addresses reserved to the first parallel port are usually: 0x378 - 0x37f. You can get the base I/O addresses reserved the parallel port(s) of your computer by issuing the command: cat /proc/ioports Here is a very sketchy description of the I/O register that is sufficient to understand our lp_led I/O driver. The interested reader can find additional information in Messmer "The indispensable PC hardware book". Assuming a 0x378 base address, the I/O addresses of the three 8-bit registers used to programm the parallel port are: 0x378: Data Register - You can write a single byte into the parallel port by (pins 2-9) issuing an outb I/O instruction on that register 0x379: Status Register - Bits 7-3 correspond respectively to pins 11, 10, 12, 13 and 15. Pin 10 correspond to a pending IRQ. The interface inverts the signal level of pin 11 (0=high, 1=low). Bits 2-0 are unused. 0x37a: Control Register - Bits 7-5 are unused. Bit 4 enables interrupt (1=enable, 0=disable). Bits 3, 1 and 0 correspond to pins 17, 14 and 1 respectively. The interface inverts the signal level for these three bits. Bit 2 corresponds to pin 16. ***************************************************************************** * PINS OF THE PARALLEL PORT USED BY THE LP_SENSOR CIRCUIT * * * ***************************************************************************** Data Register - pins 2-9 are used to read from the lp_sensor device Status Register - pin 10 is used as the IRQ line Control Register - pin 16 is used to force a Read operation, pin 17 is used to force a Write operation bit 4 is the interrupt enable flag