*****************************************************************************
*                 MAKING AN I/O DRIVER FOR A LED DEVICE                     *
*                                                                           *
*                                                                           *
*  Copyright (c)  2002  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 rudimentary non interrupt-driven
I/O driver for a simple device connected to the parallel port of the computer.


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" has already been described in
the slides.

The project consists of three 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 = kh



*****************************************************************************
STEP 1: modify the linux/arch/i386/config.in file
*****************************************************************************
add the following lines in the 'Kernel hacking' main menu:

# patch added to support the lp_led experimental device
tristate '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 line in the include/linux/major.h file:

#define LP_LED_MAJOR 42



*****************************************************************************
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 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...
#

O_TARGET := new_iodrivers.o
obj-m :=
obj-y :=
ifeq ($(CONFIG_LP_LED),y)
obj-y += lp_led.o
else
ifeq ($(CONFIG_LP_LED),m)
obj-m += lp_led.o
endif
endif
include $(TOPDIR)/Rules.make



*****************************************************************************
STEP 8: Insert a initialization function for lp_led
*****************************************************************************
a) add in drivers/char/mem.c, right after:

#if defined(CONFIG_S390_TAPE) && defined(CONFIG_S390_TAPE_CHAR)
extern void tapechar_init(void);
#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_FTAPE
	ftape_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 <linux/module.h>   /* THIS_MODULE, EXPORT_NO_SYMBOLS */
#include <linux/major.h>    /* LP_LED_MAJOR */
#include <asm/io.h>         /* inb(), outb() */

/* Addresses of the parallel port */
#define	PData   	0x378
#define	PStatus  	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(PStatus);
	 *buf = *buf & 0x78; /* clear bits 7, 2, 1, 0 */
	 *buf = *buf >> 3;   /* shift bits 6-3 into 3-0 */
	 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), PData);
	 cc = 1;
      }
   else {
         printk("KERNEL-NOTICE: when writing into lp_led count must be set to 1\n");
         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;
}

#ifdef MODULE
int init_module(void)
{
   	EXPORT_NO_SYMBOLS;
	printk("KERNEL-NOTICE: loading the lp_led module\n");
   	cc = lp_led_init();
	if (cc < 0)
        	printk("KERNEL-NOTICE: lp_led init_module error %d\n", cc);
	return cc;
}

int cleanup_module(void)
{
	printk("KERNEL-NOTICE: unloading the lp_led module\n");
        cc = unregister_chrdev(LP_LED_MAJOR, "lp_led_card");
	if (cc < 0)
        	printk("KERNEL-NOTICE: lp_led cleanup_module error %d\n", cc);
        return cc;
}
#endif

MODULE_AUTHOR("D.P. Bovet, M. Cesati, and V. Garofalo");
MODULE_DESCRIPTION("LKHC driver for lp_led character device");
MODULE_LICENSE("GPL");

*****************************************************************************
STEP 9: Test the I/O driver module:
*****************************************************************************
a) run make menuconfig and set to 'm' the CONFIG_LP_LED 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_led.o
	     
   or:
   
             modprobe lp_led
	  
   (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 the module has been loaded correctly by typing:

	     lsmod

g) run the Tlp_led test program described next


*****************************************************************************
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 might also consider changing the      */
/*   access rights of /dev/lp_led)                             */
/*   You are supposed to connect an lp_led circuit to the      */
/*   parallel port in order to get something meaningful out of */
/*   this program                                              */

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/io.h>
#include <unistd.h>
#include <fcntl.h>
#include <ctype.h>
#include <stdio.h>

int main (void)
{
	char c;
 	int cc, fd, i;
	unsigned char buf;

 	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);
			if (cc < 0) {
				printf("write error\n");
				break;
				}
		   	printf("--> hexadecimal representation of leds= %x\n\n\n", buf);
		   	c = getchar();
		   	break;

	     	case 'r':
		   	cc = read(fd, &buf, 1);
			if (cc < 0) {
				printf("read error\n");
				break;
				}
		   	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);
       		}
  	}
	return 0;
}