***************************************************************************** * MAKING AN 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.1) * ***************************************************************************** The objective is to illustrate how to make a simple non interrupt-driven I/O driver for a simple device connected to the parallel port of the computer. You'll find in the last part of this document some information on how to program the parallel port at the machine-level language. The new driver is a character device driver closely inspired to the driver included in the drivers/char/lp.c file. The I/O device that we shall baptize as "lp_led" includes a group of 8 leds that display the bytes sent from the computer and a few microswitches that can be set manually. The current microswitch setting can be read from the computer through a read() operation on the device. You'll find the circuit of "lp_led" in the lp_led_circuit.pdf file The project consists of several parts: a) create a new device file called lp_led and assign an unused major number to the new device b) define open(), close(), read(), and write() file operations for the new device and register the new character device c) test the I/O driver by running a test program that writes into and reads from the lp_led I/O device ***************************************************************************** STEP 0: set the proper EXTRAVERSION value in Makefile ***************************************************************************** replace: EXTRAVERSION = with: EXTRAVERSION = kh9 ***************************************************************************** STEP 1: modify the linux/arch/i386/config.in file ***************************************************************************** add the following lines in the 'Kernel hacking' main menu: # patch to add a new I/O driver for the lp_led device bool 'I/O driver for lp_led' CONFIG_LP_LED ***************************************************************************** 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_led device CONFIG_LP_LED If you say Y here, you will have an I/O driver capable of handling the lp_led 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/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: #ifdef CONFIG_LP_LED #define LP_LED_MAJOR 42 #endif ***************************************************************************** STEP 6: Define a new lp_led char device file with major number 42 in the /dev directory ***************************************************************************** issue as superuser the following command: mknod /dev/lp_led c 42 0 ***************************************************************************** STEP 7: Create the Makefile for the linux/new_iodrivers directory ***************************************************************************** # Makefile for linux/new_iodrivers # # Note: dependencies are done automatically by "make dep", which also removes # any old dependency. DON'T put your own dependencies ere unless it's something # special (ie not a .c file). # # Note2: the CFLAGS definition is now in the main Makefile... # # We only get into here if CONFIG_LP_LED = 'y' O_TARGET := new_iodrivers.o obj-m := obj-y := obj-y += lp_led.o include $(TOPDIR)/Rules.make ***************************************************************************** STEP 8: Insert a initialization function for lp_led ***************************************************************************** a) add in drivers/char/mem.c, right after: #ifdef CONFIG_PRINTER lp_init(); #endif the following lines: #ifdef CONFIG_LP_LED extern int lp_led_init(void); #endif b) add in drivers/char/mem.c, right after: #ifdef CONFIG_PRINTER lp_init(); #endif the following lines: #ifdef CONFIG_LP_LED lp_led_init(); #endif ***************************************************************************** STEP 9: Add the lp_led.c file in the new_iodrivers directory ***************************************************************************** /* lp_led.c */ /* */ /* This file includes all the routines needed to drive the */ /* lp_led I/O device */ #include /* THIS_MODULE, EXPORT_NO_SYMBOLS */ #include /* LP_LED_MAJOR */ #include /* inb(), outb() */ /* Addresses of the parallel port */ #define PData 0x378 #define PInput 0x379 #define PControl 0x37a static int cc; static int lp_led_open(struct inode *inode, struct file *file) { outb(0x00, PData); /* switch off the data byte (green leds) */ outb(0x04, PControl); /* switch on the power on bit (yellow led) */ return 0; } static int lp_led_close(struct inode *inode, struct file *file) { outb(0xff, PData); /* switch on the data byte (green leds) */ return 0; } static ssize_t lp_led_read(struct file *file, char *buf, size_t count, loff_t *ppos) { if (count == 1) { *buf = inb(PInput); cc = 1; } else { printk("KERNEL-NOTICE: when reading from lp_led count must be set to 1\n"); cc = -1; } return cc; } static ssize_t lp_led_write(struct file *file, char *buf, size_t count, loff_t *ppos) { if (count == 1) { outb((*buf & 0xff), PData); cc = 1; } else cc = -1; return cc; } /* this struct must appear in the code AFTER the definition of the file operations */ static struct file_operations lp_led_fops = { owner: THIS_MODULE, read: lp_led_read, write: lp_led_write, open: lp_led_open, release: lp_led_close, }; int lp_led_init(void) { cc = register_chrdev(LP_LED_MAJOR, "lp_led_card", &lp_led_fops); if (cc < 0) printk("KERNEL-NOTICE: lp_led init error %d\n", cc); return cc; } ***************************************************************************** STEP 10: Test the I/O driver by running the following program: ***************************************************************************** /* Tlp_led.c */ /* */ /* This test program for the lp_led I/O driver must run with */ /* root privilege */ /* You are supposed to connect an lp_led circuit to the */ /* parallel port in order to get something meaningful out of */ /* this program */ #include #include #include #include #include #include #include int main (void) { char c; int cc, fd, buf, i; fd = open("/dev/lp_led",O_RDWR); if (fd < 0) { printf("could not open /dev/lp_led\n"); exit(-1); } while (c!='q') { printf("\nSelect an option:\n"); printf("display a byte on the green leds: w\n"); printf("read value of microswitches: r\n"); printf("demo: d\n"); printf("quit: q\n"); c=getchar(); switch(c) { case 'w': printf("\n\n\ndigit a number in the range 0-255: "); scanf("%d", &buf); cc = write(fd, &buf, 1); printf("--> hexadecimal representation of leds= %x\n\n\n", buf); c = getchar(); break; case 'r': cc = read(fd, &buf, 1); printf ("\nchar read from lp_led = %x\n", buf); c = getchar(); break; case 'd': buf = 1; for (i=0; i<8; i++) { cc = write(fd, &buf, 1); sleep(1); buf = buf<<1; } sleep(2); buf = 255; cc = write(fd, &buf, 1); sleep(2); buf = 128; for (i=0; i<8; i++) { cc = write(fd, &buf, 1); sleep(1); buf = buf>>1; } sleep(2); buf = 255; cc = write(fd, &buf, 1); break; case 'q': cc = close(fd); } } } ***************************************************************************** * 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_LED CIRCUIT * * * ***************************************************************************** Data Register - pins 2-9 are used to write into the lp_led device Status Register - pins 10, 12, 13 and 15 are used to read data from the lp_led device (the values can be modified by acting on the microswitches). Control Register - pin 16 is used to signal whether the circuit is connected (orange led)