summaryrefslogtreecommitdiffstats
path: root/short
diff options
context:
space:
mode:
authorJavier Martinez Canillas <martinez.javier@gmail.com>2010-11-27 07:49:17 +0100
committerJavier Martinez Canillas <martinez.javier@gmail.com>2010-11-27 07:49:17 +0100
commitab121f379a3cff458c90e6f480ba4bb68c8733dd (patch)
treea9851af109ee83646d108bc247d03b131461b764 /short
downloadldd3-ab121f379a3cff458c90e6f480ba4bb68c8733dd.tar.gz
Linux Device Drivers 3 examples
Diffstat (limited to 'short')
-rw-r--r--short/Makefile40
-rw-r--r--short/short.c692
-rw-r--r--short/short_load61
-rw-r--r--short/short_unload16
4 files changed, 809 insertions, 0 deletions
diff --git a/short/Makefile b/short/Makefile
new file mode 100644
index 0000000..1655b3f
--- /dev/null
+++ b/short/Makefile
@@ -0,0 +1,40 @@
+# Comment/uncomment the following line to disable/enable debugging
+#DEBUG = y
+
+
+# Add your debugging flag (or not) to CFLAGS
+ifeq ($(DEBUG),y)
+ DEBFLAGS = -O -g -DSHORT_DEBUG # "-O" is needed to expand inlines
+else
+ DEBFLAGS = -O2
+endif
+
+CFLAGS += $(DEBFLAGS)
+CFLAGS += -I..
+
+ifneq ($(KERNELRELEASE),)
+# call from kernel build system
+
+obj-m := short.o
+
+else
+
+KERNELDIR ?= /lib/modules/$(shell uname -r)/build
+PWD := $(shell pwd)
+
+default:
+ $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
+
+endif
+
+
+clean:
+ rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
+
+depend .depend dep:
+ $(CC) $(CFLAGS) -M *.c > .depend
+
+
+ifeq (.depend,$(wildcard .depend))
+include .depend
+endif
diff --git a/short/short.c b/short/short.c
new file mode 100644
index 0000000..8bd39ed
--- /dev/null
+++ b/short/short.c
@@ -0,0 +1,692 @@
+/*
+ * short.c -- Simple Hardware Operations and Raw Tests
+ * short.c -- also a brief example of interrupt handling ("short int")
+ *
+ * Copyright (C) 2001 Alessandro Rubini and Jonathan Corbet
+ * Copyright (C) 2001 O'Reilly & Associates
+ *
+ * The source code in this file can be freely used, adapted,
+ * and redistributed in source or binary form, so long as an
+ * acknowledgment appears in derived source files. The citation
+ * should list that the code comes from the book "Linux Device
+ * Drivers" by Alessandro Rubini and Jonathan Corbet, published
+ * by O'Reilly & Associates. No warranty is attached;
+ * we cannot take responsibility for errors or fitness for use.
+ *
+ * $Id: short.c,v 1.16 2004/10/29 16:45:40 corbet Exp $
+ */
+
+/*
+ * FIXME: this driver is not safe with concurrent readers or
+ * writers.
+ */
+
+#include <linux/config.h>
+#include <linux/module.h>
+#include <linux/moduleparam.h>
+#include <linux/init.h>
+
+#include <linux/sched.h>
+#include <linux/kernel.h> /* printk() */
+#include <linux/fs.h> /* everything... */
+#include <linux/errno.h> /* error codes */
+#include <linux/delay.h> /* udelay */
+#include <linux/kdev_t.h>
+#include <linux/slab.h>
+#include <linux/mm.h>
+#include <linux/ioport.h>
+#include <linux/interrupt.h>
+#include <linux/workqueue.h>
+#include <linux/poll.h>
+#include <linux/wait.h>
+
+#include <asm/io.h>
+
+#define SHORT_NR_PORTS 8 /* use 8 ports by default */
+
+/*
+ * all of the parameters have no "short_" prefix, to save typing when
+ * specifying them at load time
+ */
+static int major = 0; /* dynamic by default */
+module_param(major, int, 0);
+
+static int use_mem = 0; /* default is I/O-mapped */
+module_param(use_mem, int, 0);
+
+/* default is the first printer port on PC's. "short_base" is there too
+ because it's what we want to use in the code */
+static unsigned long base = 0x378;
+unsigned long short_base = 0;
+module_param(base, long, 0);
+
+/* The interrupt line is undefined by default. "short_irq" is as above */
+static int irq = -1;
+volatile int short_irq = -1;
+module_param(irq, int, 0);
+
+static int probe = 0; /* select at load time how to probe irq line */
+module_param(probe, int, 0);
+
+static int wq = 0; /* select at load time whether a workqueue is used */
+module_param(wq, int, 0);
+
+static int tasklet = 0; /* select whether a tasklet is used */
+module_param(tasklet, int, 0);
+
+static int share = 0; /* select at load time whether install a shared irq */
+module_param(share, int, 0);
+
+MODULE_AUTHOR ("Alessandro Rubini");
+MODULE_LICENSE("Dual BSD/GPL");
+
+
+unsigned long short_buffer = 0;
+unsigned long volatile short_head;
+volatile unsigned long short_tail;
+DECLARE_WAIT_QUEUE_HEAD(short_queue);
+
+/* Set up our tasklet if we're doing that. */
+void short_do_tasklet(unsigned long);
+DECLARE_TASKLET(short_tasklet, short_do_tasklet, 0);
+
+/*
+ * Atomicly increment an index into short_buffer
+ */
+static inline void short_incr_bp(volatile unsigned long *index, int delta)
+{
+ unsigned long new = *index + delta;
+ barrier(); /* Don't optimize these two together */
+ *index = (new >= (short_buffer + PAGE_SIZE)) ? short_buffer : new;
+}
+
+
+/*
+ * The devices with low minor numbers write/read burst of data to/from
+ * specific I/O ports (by default the parallel ones).
+ *
+ * The device with 128 as minor number returns ascii strings telling
+ * when interrupts have been received. Writing to the device toggles
+ * 00/FF on the parallel data lines. If there is a loopback wire, this
+ * generates interrupts.
+ */
+
+int short_open (struct inode *inode, struct file *filp)
+{
+ extern struct file_operations short_i_fops;
+
+ if (iminor (inode) & 0x80)
+ filp->f_op = &short_i_fops; /* the interrupt-driven node */
+ return 0;
+}
+
+
+int short_release (struct inode *inode, struct file *filp)
+{
+ return 0;
+}
+
+
+/* first, the port-oriented device */
+
+enum short_modes {SHORT_DEFAULT=0, SHORT_PAUSE, SHORT_STRING, SHORT_MEMORY};
+
+ssize_t do_short_read (struct inode *inode, struct file *filp, char __user *buf,
+ size_t count, loff_t *f_pos)
+{
+ int retval = count, minor = iminor (inode);
+ unsigned long port = short_base + (minor&0x0f);
+ void *address = (void *) short_base + (minor&0x0f);
+ int mode = (minor&0x70) >> 4;
+ unsigned char *kbuf = kmalloc(count, GFP_KERNEL), *ptr;
+
+ if (!kbuf)
+ return -ENOMEM;
+ ptr = kbuf;
+
+ if (use_mem)
+ mode = SHORT_MEMORY;
+
+ switch(mode) {
+ case SHORT_STRING:
+ insb(port, ptr, count);
+ rmb();
+ break;
+
+ case SHORT_DEFAULT:
+ while (count--) {
+ *(ptr++) = inb(port);
+ rmb();
+ }
+ break;
+
+ case SHORT_MEMORY:
+ while (count--) {
+ *ptr++ = ioread8(address);
+ rmb();
+ }
+ break;
+ case SHORT_PAUSE:
+ while (count--) {
+ *(ptr++) = inb_p(port);
+ rmb();
+ }
+ break;
+
+ default: /* no more modes defined by now */
+ retval = -EINVAL;
+ break;
+ }
+ if ((retval > 0) && copy_to_user(buf, kbuf, retval))
+ retval = -EFAULT;
+ kfree(kbuf);
+ return retval;
+}
+
+
+/*
+ * Version-specific methods for the fops structure. FIXME don't need anymore.
+ */
+ssize_t short_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
+{
+ return do_short_read(filp->f_dentry->d_inode, filp, buf, count, f_pos);
+}
+
+
+
+ssize_t do_short_write (struct inode *inode, struct file *filp, const char __user *buf,
+ size_t count, loff_t *f_pos)
+{
+ int retval = count, minor = iminor(inode);
+ unsigned long port = short_base + (minor&0x0f);
+ void *address = (void *) short_base + (minor&0x0f);
+ int mode = (minor&0x70) >> 4;
+ unsigned char *kbuf = kmalloc(count, GFP_KERNEL), *ptr;
+
+ if (!kbuf)
+ return -ENOMEM;
+ if (copy_from_user(kbuf, buf, count))
+ return -EFAULT;
+ ptr = kbuf;
+
+ if (use_mem)
+ mode = SHORT_MEMORY;
+
+ switch(mode) {
+ case SHORT_PAUSE:
+ while (count--) {
+ outb_p(*(ptr++), port);
+ wmb();
+ }
+ break;
+
+ case SHORT_STRING:
+ outsb(port, ptr, count);
+ wmb();
+ break;
+
+ case SHORT_DEFAULT:
+ while (count--) {
+ outb(*(ptr++), port);
+ wmb();
+ }
+ break;
+
+ case SHORT_MEMORY:
+ while (count--) {
+ iowrite8(*ptr++, address);
+ wmb();
+ }
+ break;
+
+ default: /* no more modes defined by now */
+ retval = -EINVAL;
+ break;
+ }
+ kfree(kbuf);
+ return retval;
+}
+
+
+ssize_t short_write(struct file *filp, const char __user *buf, size_t count,
+ loff_t *f_pos)
+{
+ return do_short_write(filp->f_dentry->d_inode, filp, buf, count, f_pos);
+}
+
+
+
+
+unsigned int short_poll(struct file *filp, poll_table *wait)
+{
+ return POLLIN | POLLRDNORM | POLLOUT | POLLWRNORM;
+}
+
+
+
+
+
+
+struct file_operations short_fops = {
+ .owner = THIS_MODULE,
+ .read = short_read,
+ .write = short_write,
+ .poll = short_poll,
+ .open = short_open,
+ .release = short_release,
+};
+
+/* then, the interrupt-related device */
+
+ssize_t short_i_read (struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
+{
+ int count0;
+ DEFINE_WAIT(wait);
+
+ while (short_head == short_tail) {
+ prepare_to_wait(&short_queue, &wait, TASK_INTERRUPTIBLE);
+ if (short_head == short_tail)
+ schedule();
+ finish_wait(&short_queue, &wait);
+ if (signal_pending (current)) /* a signal arrived */
+ return -ERESTARTSYS; /* tell the fs layer to handle it */
+ }
+ /* count0 is the number of readable data bytes */
+ count0 = short_head - short_tail;
+ if (count0 < 0) /* wrapped */
+ count0 = short_buffer + PAGE_SIZE - short_tail;
+ if (count0 < count) count = count0;
+
+ if (copy_to_user(buf, (char *)short_tail, count))
+ return -EFAULT;
+ short_incr_bp (&short_tail, count);
+ return count;
+}
+
+ssize_t short_i_write (struct file *filp, const char __user *buf, size_t count,
+ loff_t *f_pos)
+{
+ int written = 0, odd = *f_pos & 1;
+ unsigned long port = short_base; /* output to the parallel data latch */
+ void *address = (void *) short_base;
+
+ if (use_mem) {
+ while (written < count)
+ iowrite8(0xff * ((++written + odd) & 1), address);
+ } else {
+ while (written < count)
+ outb(0xff * ((++written + odd) & 1), port);
+ }
+
+ *f_pos += count;
+ return written;
+}
+
+
+
+
+struct file_operations short_i_fops = {
+ .owner = THIS_MODULE,
+ .read = short_i_read,
+ .write = short_i_write,
+ .open = short_open,
+ .release = short_release,
+};
+
+irqreturn_t short_interrupt(int irq, void *dev_id, struct pt_regs *regs)
+{
+ struct timeval tv;
+ int written;
+
+ do_gettimeofday(&tv);
+
+ /* Write a 16 byte record. Assume PAGE_SIZE is a multiple of 16 */
+ written = sprintf((char *)short_head,"%08u.%06u\n",
+ (int)(tv.tv_sec % 100000000), (int)(tv.tv_usec));
+ BUG_ON(written != 16);
+ short_incr_bp(&short_head, written);
+ wake_up_interruptible(&short_queue); /* awake any reading process */
+ return IRQ_HANDLED;
+}
+
+/*
+ * The following two functions are equivalent to the previous one,
+ * but split in top and bottom half. First, a few needed variables
+ */
+
+#define NR_TIMEVAL 512 /* length of the array of time values */
+
+struct timeval tv_data[NR_TIMEVAL]; /* too lazy to allocate it */
+volatile struct timeval *tv_head=tv_data;
+volatile struct timeval *tv_tail=tv_data;
+
+static struct work_struct short_wq;
+
+
+int short_wq_count = 0;
+
+/*
+ * Increment a circular buffer pointer in a way that nobody sees
+ * an intermediate value.
+ */
+static inline void short_incr_tv(volatile struct timeval **tvp)
+{
+ if (*tvp == (tv_data + NR_TIMEVAL - 1))
+ *tvp = tv_data; /* Wrap */
+ else
+ (*tvp)++;
+}
+
+
+
+void short_do_tasklet (unsigned long unused)
+{
+ int savecount = short_wq_count, written;
+ short_wq_count = 0; /* we have already been removed from the queue */
+ /*
+ * The bottom half reads the tv array, filled by the top half,
+ * and prints it to the circular text buffer, which is then consumed
+ * by reading processes
+ */
+
+ /* First write the number of interrupts that occurred before this bh */
+ written = sprintf((char *)short_head,"bh after %6i\n",savecount);
+ short_incr_bp(&short_head, written);
+
+ /*
+ * Then, write the time values. Write exactly 16 bytes at a time,
+ * so it aligns with PAGE_SIZE
+ */
+
+ do {
+ written = sprintf((char *)short_head,"%08u.%06u\n",
+ (int)(tv_tail->tv_sec % 100000000),
+ (int)(tv_tail->tv_usec));
+ short_incr_bp(&short_head, written);
+ short_incr_tv(&tv_tail);
+ } while (tv_tail != tv_head);
+
+ wake_up_interruptible(&short_queue); /* awake any reading process */
+}
+
+
+irqreturn_t short_wq_interrupt(int irq, void *dev_id, struct pt_regs *regs)
+{
+ /* Grab the current time information. */
+ do_gettimeofday((struct timeval *) tv_head);
+ short_incr_tv(&tv_head);
+
+ /* Queue the bh. Don't worry about multiple enqueueing */
+ schedule_work(&short_wq);
+
+ short_wq_count++; /* record that an interrupt arrived */
+ return IRQ_HANDLED;
+}
+
+
+/*
+ * Tasklet top half
+ */
+
+irqreturn_t short_tl_interrupt(int irq, void *dev_id, struct pt_regs *regs)
+{
+ do_gettimeofday((struct timeval *) tv_head); /* cast to stop 'volatile' warning */
+ short_incr_tv(&tv_head);
+ tasklet_schedule(&short_tasklet);
+ short_wq_count++; /* record that an interrupt arrived */
+ return IRQ_HANDLED;
+}
+
+
+
+
+irqreturn_t short_sh_interrupt(int irq, void *dev_id, struct pt_regs *regs)
+{
+ int value, written;
+ struct timeval tv;
+
+ /* If it wasn't short, return immediately */
+ value = inb(short_base);
+ if (!(value & 0x80))
+ return IRQ_NONE;
+
+ /* clear the interrupting bit */
+ outb(value & 0x7F, short_base);
+
+ /* the rest is unchanged */
+
+ do_gettimeofday(&tv);
+ written = sprintf((char *)short_head,"%08u.%06u\n",
+ (int)(tv.tv_sec % 100000000), (int)(tv.tv_usec));
+ short_incr_bp(&short_head, written);
+ wake_up_interruptible(&short_queue); /* awake any reading process */
+ return IRQ_HANDLED;
+}
+
+void short_kernelprobe(void)
+{
+ int count = 0;
+ do {
+ unsigned long mask;
+
+ mask = probe_irq_on();
+ outb_p(0x10,short_base+2); /* enable reporting */
+ outb_p(0x00,short_base); /* clear the bit */
+ outb_p(0xFF,short_base); /* set the bit: interrupt! */
+ outb_p(0x00,short_base+2); /* disable reporting */
+ udelay(5); /* give it some time */
+ short_irq = probe_irq_off(mask);
+
+ if (short_irq == 0) { /* none of them? */
+ printk(KERN_INFO "short: no irq reported by probe\n");
+ short_irq = -1;
+ }
+ /*
+ * if more than one line has been activated, the result is
+ * negative. We should service the interrupt (no need for lpt port)
+ * and loop over again. Loop at most five times, then give up
+ */
+ } while (short_irq < 0 && count++ < 5);
+ if (short_irq < 0)
+ printk("short: probe failed %i times, giving up\n", count);
+}
+
+irqreturn_t short_probing(int irq, void *dev_id, struct pt_regs *regs)
+{
+ if (short_irq == 0) short_irq = irq; /* found */
+ if (short_irq != irq) short_irq = -irq; /* ambiguous */
+ return IRQ_HANDLED;
+}
+
+void short_selfprobe(void)
+{
+ int trials[] = {3, 5, 7, 9, 0};
+ int tried[] = {0, 0, 0, 0, 0};
+ int i, count = 0;
+
+ /*
+ * install the probing handler for all possible lines. Remember
+ * the result (0 for success, or -EBUSY) in order to only free
+ * what has been acquired
+ */
+ for (i = 0; trials[i]; i++)
+ tried[i] = request_irq(trials[i], short_probing,
+ SA_INTERRUPT, "short probe", NULL);
+
+ do {
+ short_irq = 0; /* none got, yet */
+ outb_p(0x10,short_base+2); /* enable */
+ outb_p(0x00,short_base);
+ outb_p(0xFF,short_base); /* toggle the bit */
+ outb_p(0x00,short_base+2); /* disable */
+ udelay(5); /* give it some time */
+
+ /* the value has been set by the handler */
+ if (short_irq == 0) { /* none of them? */
+ printk(KERN_INFO "short: no irq reported by probe\n");
+ }
+ /*
+ * If more than one line has been activated, the result is
+ * negative. We should service the interrupt (but the lpt port
+ * doesn't need it) and loop over again. Do it at most 5 times
+ */
+ } while (short_irq <=0 && count++ < 5);
+
+ /* end of loop, uninstall the handler */
+ for (i = 0; trials[i]; i++)
+ if (tried[i] == 0)
+ free_irq(trials[i], NULL);
+
+ if (short_irq < 0)
+ printk("short: probe failed %i times, giving up\n", count);
+}
+
+
+
+/* Finally, init and cleanup */
+
+int short_init(void)
+{
+ int result;
+
+ /*
+ * first, sort out the base/short_base ambiguity: we'd better
+ * use short_base in the code, for clarity, but allow setting
+ * just "base" at load time. Same for "irq".
+ */
+ short_base = base;
+ short_irq = irq;
+
+ /* Get our needed resources. */
+ if (!use_mem) {
+ if (! request_region(short_base, SHORT_NR_PORTS, "short")) {
+ printk(KERN_INFO "short: can't get I/O port address 0x%lx\n",
+ short_base);
+ return -ENODEV;
+ }
+
+ } else {
+ if (! request_mem_region(short_base, SHORT_NR_PORTS, "short")) {
+ printk(KERN_INFO "short: can't get I/O mem address 0x%lx\n",
+ short_base);
+ return -ENODEV;
+ }
+
+ /* also, ioremap it */
+ short_base = (unsigned long) ioremap(short_base, SHORT_NR_PORTS);
+ /* Hmm... we should check the return value */
+ }
+ /* Here we register our device - should not fail thereafter */
+ result = register_chrdev(major, "short", &short_fops);
+ if (result < 0) {
+ printk(KERN_INFO "short: can't get major number\n");
+ release_region(short_base,SHORT_NR_PORTS); /* FIXME - use-mem case? */
+ return result;
+ }
+ if (major == 0) major = result; /* dynamic */
+
+ short_buffer = __get_free_pages(GFP_KERNEL,0); /* never fails */ /* FIXME */
+ short_head = short_tail = short_buffer;
+
+ /*
+ * Fill the workqueue structure, used for the bottom half handler.
+ * The cast is there to prevent warnings about the type of the
+ * (unused) argument.
+ */
+ /* this line is in short_init() */
+ INIT_WORK(&short_wq, (void (*)(void *)) short_do_tasklet, NULL);
+
+ /*
+ * Now we deal with the interrupt: either kernel-based
+ * autodetection, DIY detection or default number
+ */
+
+ if (short_irq < 0 && probe == 1)
+ short_kernelprobe();
+
+ if (short_irq < 0 && probe == 2)
+ short_selfprobe();
+
+ if (short_irq < 0) /* not yet specified: force the default on */
+ switch(short_base) {
+ case 0x378: short_irq = 7; break;
+ case 0x278: short_irq = 2; break;
+ case 0x3bc: short_irq = 5; break;
+ }
+
+ /*
+ * If shared has been specified, installed the shared handler
+ * instead of the normal one. Do it first, before a -EBUSY will
+ * force short_irq to -1.
+ */
+ if (short_irq >= 0 && share > 0) {
+ result = request_irq(short_irq, short_sh_interrupt,
+ SA_SHIRQ | SA_INTERRUPT,"short",
+ short_sh_interrupt);
+ if (result) {
+ printk(KERN_INFO "short: can't get assigned irq %i\n", short_irq);
+ short_irq = -1;
+ }
+ else { /* actually enable it -- assume this *is* a parallel port */
+ outb(0x10, short_base+2);
+ }
+ return 0; /* the rest of the function only installs handlers */
+ }
+
+ if (short_irq >= 0) {
+ result = request_irq(short_irq, short_interrupt,
+ SA_INTERRUPT, "short", NULL);
+ if (result) {
+ printk(KERN_INFO "short: can't get assigned irq %i\n",
+ short_irq);
+ short_irq = -1;
+ }
+ else { /* actually enable it -- assume this *is* a parallel port */
+ outb(0x10,short_base+2);
+ }
+ }
+
+ /*
+ * Ok, now change the interrupt handler if using top/bottom halves
+ * has been requested
+ */
+ if (short_irq >= 0 && (wq + tasklet) > 0) {
+ free_irq(short_irq,NULL);
+ result = request_irq(short_irq,
+ tasklet ? short_tl_interrupt :
+ short_wq_interrupt,
+ SA_INTERRUPT,"short-bh", NULL);
+ if (result) {
+ printk(KERN_INFO "short-bh: can't get assigned irq %i\n",
+ short_irq);
+ short_irq = -1;
+ }
+ }
+
+ return 0;
+}
+
+void short_cleanup(void)
+{
+ if (short_irq >= 0) {
+ outb(0x0, short_base + 2); /* disable the interrupt */
+ if (!share) free_irq(short_irq, NULL);
+ else free_irq(short_irq, short_sh_interrupt);
+ }
+ /* Make sure we don't leave work queue/tasklet functions running */
+ if (tasklet)
+ tasklet_disable(&short_tasklet);
+ else
+ flush_scheduled_work();
+ unregister_chrdev(major, "short");
+ if (use_mem) {
+ iounmap((void __iomem *)short_base);
+ release_mem_region(short_base, SHORT_NR_PORTS);
+ } else {
+ release_region(short_base,SHORT_NR_PORTS);
+ }
+ if (short_buffer) free_page(short_buffer);
+}
+
+module_init(short_init);
+module_exit(short_cleanup);
diff --git a/short/short_load b/short/short_load
new file mode 100644
index 0000000..407846c
--- /dev/null
+++ b/short/short_load
@@ -0,0 +1,61 @@
+#!/bin/sh
+module="short"
+device="short"
+mode="664"
+
+# Group: since distributions do it differently, look for wheel or use staff
+if grep '^staff:' /etc/group > /dev/null; then
+ group="staff"
+else
+ group="wheel"
+fi
+
+
+# invoke insmod with all arguments we got
+# and use a pathname, as newer modutils don't look in . by default
+/sbin/insmod ./$module.ko $* || exit 1
+
+major=`cat /proc/devices | awk "\\$2==\"$module\" {print \\$1}"`
+
+# Create 8 entry points, as SHORT_NR_PORTS is 8 by default
+rm -f /dev/${device}[0-7]
+mknod /dev/${device}0 c $major 0
+mknod /dev/${device}1 c $major 1
+mknod /dev/${device}2 c $major 2
+mknod /dev/${device}3 c $major 3
+mknod /dev/${device}4 c $major 4
+mknod /dev/${device}5 c $major 5
+mknod /dev/${device}6 c $major 6
+mknod /dev/${device}7 c $major 7
+
+rm -f /dev/${device}[0-3][ps]
+mknod /dev/${device}0p c $major 16
+mknod /dev/${device}1p c $major 17
+mknod /dev/${device}2p c $major 18
+mknod /dev/${device}3p c $major 19
+mknod /dev/${device}4p c $major 20
+mknod /dev/${device}5p c $major 21
+mknod /dev/${device}6p c $major 22
+mknod /dev/${device}7p c $major 23
+
+mknod /dev/${device}0s c $major 32
+mknod /dev/${device}1s c $major 33
+mknod /dev/${device}2s c $major 34
+mknod /dev/${device}3s c $major 35
+mknod /dev/${device}4s c $major 36
+mknod /dev/${device}5s c $major 37
+mknod /dev/${device}6s c $major 38
+mknod /dev/${device}7s c $major 39
+
+rm -f /dev/${device}int /dev/${device}print
+mknod /dev/${device}int c $major 128
+mknod /dev/${device}print c $major 129
+
+chgrp $group /dev/${device}[0-7] /dev/${device}[0-7][ps] /dev/${device}int
+chmod $mode /dev/${device}[0-7] /dev/${device}[0-7][ps] /dev/${device}int
+
+
+
+
+
+
diff --git a/short/short_unload b/short/short_unload
new file mode 100644
index 0000000..7cf64dc
--- /dev/null
+++ b/short/short_unload
@@ -0,0 +1,16 @@
+#!/bin/sh
+module="short"
+device="short"
+
+# invoke rmmod with all arguments we got
+/sbin/rmmod $module $* || exit 1
+
+# Remove stale nodes
+
+rm -f /dev/${device}[0-7] /dev/${device}[0-7][ps] \
+ /dev/${device}int /dev/${device}print
+
+
+
+
+