diff options
author | Javier Martinez Canillas <martinez.javier@gmail.com> | 2010-11-27 07:49:17 +0100 |
---|---|---|
committer | Javier Martinez Canillas <martinez.javier@gmail.com> | 2010-11-27 07:49:17 +0100 |
commit | ab121f379a3cff458c90e6f480ba4bb68c8733dd (patch) | |
tree | a9851af109ee83646d108bc247d03b131461b764 /short/short.c | |
download | ldd3-ab121f379a3cff458c90e6f480ba4bb68c8733dd.tar.gz |
Linux Device Drivers 3 examples
Diffstat (limited to 'short/short.c')
-rw-r--r-- | short/short.c | 692 |
1 files changed, 692 insertions, 0 deletions
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); |