| /* radio-cadet.c - A video4linux driver for the ADS Cadet AM/FM Radio Card | 
 |  * | 
 |  * by Fred Gleason <fredg@wava.com> | 
 |  * Version 0.3.3 | 
 |  * | 
 |  * (Loosely) based on code for the Aztech radio card by | 
 |  * | 
 |  * Russell Kroll    (rkroll@exploits.org) | 
 |  * Quay Ly | 
 |  * Donald Song | 
 |  * Jason Lewis      (jlewis@twilight.vtc.vsc.edu)  | 
 |  * Scott McGrath    (smcgrath@twilight.vtc.vsc.edu) | 
 |  * William McGrath  (wmcgrath@twilight.vtc.vsc.edu) | 
 |  * | 
 |  * History: | 
 |  * 2000-04-29	Russell Kroll <rkroll@exploits.org> | 
 |  *		Added ISAPnP detection for Linux 2.3/2.4 | 
 |  * | 
 |  * 2001-01-10	Russell Kroll <rkroll@exploits.org> | 
 |  *		Removed dead CONFIG_RADIO_CADET_PORT code | 
 |  *		PnP detection on load is now default (no args necessary) | 
 |  * | 
 |  * 2002-01-17	Adam Belay <ambx1@neo.rr.com> | 
 |  *		Updated to latest pnp code | 
 |  * | 
 |  * 2003-01-31	Alan Cox <alan@redhat.com> | 
 |  *		Cleaned up locking, delay code, general odds and ends | 
 |  */ | 
 |  | 
 | #include <linux/module.h>	/* Modules 			*/ | 
 | #include <linux/init.h>		/* Initdata			*/ | 
 | #include <linux/ioport.h>	/* check_region, request_region	*/ | 
 | #include <linux/delay.h>	/* udelay			*/ | 
 | #include <asm/io.h>		/* outb, outb_p			*/ | 
 | #include <asm/uaccess.h>	/* copy to/from user		*/ | 
 | #include <linux/videodev.h>	/* kernel radio structs		*/ | 
 | #include <linux/param.h> | 
 | #include <linux/pnp.h> | 
 |  | 
 | #define RDS_BUFFER 256 | 
 |  | 
 | static int io=-1;		/* default to isapnp activation */ | 
 | static int radio_nr = -1; | 
 | static int users=0; | 
 | static int curtuner=0; | 
 | static int tunestat=0; | 
 | static int sigstrength=0; | 
 | static wait_queue_head_t read_queue; | 
 | static struct timer_list readtimer; | 
 | static __u8 rdsin=0,rdsout=0,rdsstat=0; | 
 | static unsigned char rdsbuf[RDS_BUFFER]; | 
 | static spinlock_t cadet_io_lock; | 
 |  | 
 | static int cadet_probe(void); | 
 |  | 
 | /* | 
 |  * Signal Strength Threshold Values | 
 |  * The V4L API spec does not define any particular unit for the signal  | 
 |  * strength value.  These values are in microvolts of RF at the tuner's input. | 
 |  */ | 
 | static __u16 sigtable[2][4]={{5,10,30,150},{28,40,63,1000}}; | 
 |  | 
 | static int cadet_getrds(void) | 
 | { | 
 |         int rdsstat=0; | 
 |  | 
 | 	spin_lock(&cadet_io_lock); | 
 |         outb(3,io);                 /* Select Decoder Control/Status */ | 
 | 	outb(inb(io+1)&0x7f,io+1);  /* Reset RDS detection */ | 
 | 	spin_unlock(&cadet_io_lock); | 
 | 	 | 
 | 	msleep(100); | 
 |  | 
 | 	spin_lock(&cadet_io_lock);	 | 
 |         outb(3,io);                 /* Select Decoder Control/Status */ | 
 | 	if((inb(io+1)&0x80)!=0) { | 
 | 	        rdsstat|=VIDEO_TUNER_RDS_ON; | 
 | 	} | 
 | 	if((inb(io+1)&0x10)!=0) { | 
 | 	        rdsstat|=VIDEO_TUNER_MBS_ON; | 
 | 	} | 
 | 	spin_unlock(&cadet_io_lock); | 
 | 	return rdsstat; | 
 | } | 
 |  | 
 | static int cadet_getstereo(void) | 
 | { | 
 | 	int ret = 0; | 
 |         if(curtuner != 0)	/* Only FM has stereo capability! */ | 
 | 	        return 0; | 
 |  | 
 | 	spin_lock(&cadet_io_lock); | 
 |         outb(7,io);          /* Select tuner control */ | 
 | 	if( (inb(io+1) & 0x40) == 0) | 
 |         	ret = 1; | 
 |         spin_unlock(&cadet_io_lock); | 
 |         return ret; | 
 | } | 
 |  | 
 | static unsigned cadet_gettune(void) | 
 | { | 
 |         int curvol,i; | 
 | 	unsigned fifo=0; | 
 |  | 
 |         /* | 
 |          * Prepare for read | 
 |          */ | 
 |  | 
 | 	spin_lock(&cadet_io_lock); | 
 | 	 | 
 |         outb(7,io);       /* Select tuner control */ | 
 |         curvol=inb(io+1); /* Save current volume/mute setting */ | 
 |         outb(0x00,io+1);  /* Ensure WRITE-ENABLE is LOW */ | 
 | 	tunestat=0xffff; | 
 |  | 
 |         /* | 
 |          * Read the shift register | 
 |          */ | 
 |         for(i=0;i<25;i++) { | 
 |                 fifo=(fifo<<1)|((inb(io+1)>>7)&0x01); | 
 |                 if(i<24) { | 
 |                         outb(0x01,io+1); | 
 | 			tunestat&=inb(io+1); | 
 |                         outb(0x00,io+1); | 
 |                 } | 
 |         } | 
 |  | 
 |         /* | 
 |          * Restore volume/mute setting | 
 |          */ | 
 |         outb(curvol,io+1); | 
 | 	spin_unlock(&cadet_io_lock); | 
 |  | 
 | 	return fifo; | 
 | } | 
 |  | 
 | static unsigned cadet_getfreq(void) | 
 | { | 
 |         int i; | 
 |         unsigned freq=0,test,fifo=0; | 
 |  | 
 | 	/* | 
 | 	 * Read current tuning | 
 | 	 */ | 
 | 	fifo=cadet_gettune(); | 
 |  | 
 |         /* | 
 |          * Convert to actual frequency | 
 |          */ | 
 | 	if(curtuner==0) {    /* FM */ | 
 | 	        test=12500; | 
 |                 for(i=0;i<14;i++) { | 
 |                         if((fifo&0x01)!=0) { | 
 |                                 freq+=test; | 
 |                         } | 
 |                         test=test<<1; | 
 |                         fifo=fifo>>1; | 
 |                 } | 
 |                 freq-=10700000;           /* IF frequency is 10.7 MHz */ | 
 |                 freq=(freq*16)/1000000;   /* Make it 1/16 MHz */ | 
 | 	} | 
 | 	if(curtuner==1) {    /* AM */ | 
 | 	        freq=((fifo&0x7fff)-2010)*16; | 
 | 	} | 
 |  | 
 |         return freq; | 
 | } | 
 |  | 
 | static void cadet_settune(unsigned fifo) | 
 | { | 
 |         int i; | 
 | 	unsigned test;   | 
 |  | 
 | 	spin_lock(&cadet_io_lock); | 
 | 	 | 
 | 	outb(7,io);                /* Select tuner control */ | 
 | 	/* | 
 | 	 * Write the shift register | 
 | 	 */ | 
 | 	test=0; | 
 | 	test=(fifo>>23)&0x02;      /* Align data for SDO */ | 
 | 	test|=0x1c;                /* SDM=1, SWE=1, SEN=1, SCK=0 */ | 
 | 	outb(7,io);                /* Select tuner control */ | 
 | 	outb(test,io+1);           /* Initialize for write */ | 
 | 	for(i=0;i<25;i++) { | 
 |    	        test|=0x01;              /* Toggle SCK High */ | 
 | 		outb(test,io+1); | 
 | 		test&=0xfe;              /* Toggle SCK Low */ | 
 | 		outb(test,io+1); | 
 | 		fifo=fifo<<1;            /* Prepare the next bit */ | 
 | 		test=0x1c|((fifo>>23)&0x02); | 
 | 		outb(test,io+1); | 
 | 	} | 
 | 	spin_unlock(&cadet_io_lock); | 
 | } | 
 |  | 
 | static void cadet_setfreq(unsigned freq) | 
 | { | 
 |         unsigned fifo; | 
 |         int i,j,test; | 
 |         int curvol; | 
 |  | 
 |         /*  | 
 |          * Formulate a fifo command | 
 |          */ | 
 | 	fifo=0; | 
 | 	if(curtuner==0) {    /* FM */ | 
 |         	test=102400; | 
 |                 freq=(freq*1000)/16;       /* Make it kHz */ | 
 |                 freq+=10700;               /* IF is 10700 kHz */ | 
 |                 for(i=0;i<14;i++) { | 
 |                         fifo=fifo<<1; | 
 |                         if(freq>=test) { | 
 |                                 fifo|=0x01; | 
 |                                 freq-=test; | 
 |                         } | 
 |                         test=test>>1; | 
 |                 } | 
 | 	} | 
 | 	if(curtuner==1) {    /* AM */ | 
 |                 fifo=(freq/16)+2010;            /* Make it kHz */ | 
 | 		fifo|=0x100000;            /* Select AM Band */ | 
 | 	} | 
 |  | 
 |         /* | 
 |          * Save current volume/mute setting | 
 |          */ | 
 |  | 
 | 	spin_lock(&cadet_io_lock); | 
 | 	outb(7,io);                /* Select tuner control */ | 
 |         curvol=inb(io+1);  | 
 |         spin_unlock(&cadet_io_lock); | 
 |  | 
 | 	/* | 
 | 	 * Tune the card | 
 | 	 */ | 
 | 	for(j=3;j>-1;j--) { | 
 | 	        cadet_settune(fifo|(j<<16)); | 
 | 	         | 
 | 	        spin_lock(&cadet_io_lock); | 
 | 		outb(7,io);         /* Select tuner control */ | 
 | 		outb(curvol,io+1); | 
 | 		spin_unlock(&cadet_io_lock); | 
 | 		 | 
 | 		msleep(100); | 
 |  | 
 | 		cadet_gettune(); | 
 | 		if((tunestat & 0x40) == 0) {   /* Tuned */ | 
 | 		        sigstrength=sigtable[curtuner][j]; | 
 | 			return; | 
 | 		} | 
 | 	} | 
 | 	sigstrength=0; | 
 | } | 
 |  | 
 |  | 
 | static int cadet_getvol(void) | 
 | { | 
 | 	int ret = 0; | 
 | 	 | 
 | 	spin_lock(&cadet_io_lock); | 
 | 	 | 
 |         outb(7,io);                /* Select tuner control */ | 
 |         if((inb(io + 1) & 0x20) != 0) | 
 |         	ret = 0xffff; | 
 |          | 
 |         spin_unlock(&cadet_io_lock); | 
 |         return ret; | 
 | } | 
 |  | 
 |  | 
 | static void cadet_setvol(int vol) | 
 | { | 
 | 	spin_lock(&cadet_io_lock); | 
 |         outb(7,io);                /* Select tuner control */ | 
 |         if(vol>0) | 
 |                 outb(0x20,io+1); | 
 |         else | 
 |                 outb(0x00,io+1); | 
 | 	spin_unlock(&cadet_io_lock); | 
 | }   | 
 |  | 
 | static void cadet_handler(unsigned long data) | 
 | { | 
 | 	/* | 
 | 	 * Service the RDS fifo | 
 | 	 */ | 
 |  | 
 | 	if(spin_trylock(&cadet_io_lock)) | 
 | 	{ | 
 | 	        outb(0x3,io);       /* Select RDS Decoder Control */ | 
 | 		if((inb(io+1)&0x20)!=0) { | 
 | 		        printk(KERN_CRIT "cadet: RDS fifo overflow\n"); | 
 | 		} | 
 | 		outb(0x80,io);      /* Select RDS fifo */ | 
 | 		while((inb(io)&0x80)!=0) { | 
 | 		        rdsbuf[rdsin]=inb(io+1); | 
 | 			if(rdsin==rdsout) | 
 | 			        printk(KERN_WARNING "cadet: RDS buffer overflow\n"); | 
 | 			else | 
 | 				rdsin++; | 
 | 		} | 
 | 		spin_unlock(&cadet_io_lock); | 
 | 	} | 
 |  | 
 | 	/* | 
 | 	 * Service pending read | 
 | 	 */ | 
 | 	if( rdsin!=rdsout) | 
 | 	        wake_up_interruptible(&read_queue); | 
 |  | 
 | 	/*  | 
 | 	 * Clean up and exit | 
 | 	 */ | 
 | 	init_timer(&readtimer); | 
 | 	readtimer.function=cadet_handler; | 
 | 	readtimer.data=(unsigned long)0; | 
 | 	readtimer.expires=jiffies+(HZ/20); | 
 | 	add_timer(&readtimer); | 
 | } | 
 |  | 
 |  | 
 |  | 
 | static ssize_t cadet_read(struct file *file, char __user *data, | 
 | 			  size_t count, loff_t *ppos) | 
 | { | 
 |         int i=0; | 
 | 	unsigned char readbuf[RDS_BUFFER]; | 
 |  | 
 |         if(rdsstat==0) { | 
 | 		spin_lock(&cadet_io_lock); | 
 | 	        rdsstat=1; | 
 | 		outb(0x80,io);        /* Select RDS fifo */ | 
 | 		spin_unlock(&cadet_io_lock); | 
 | 		init_timer(&readtimer); | 
 | 		readtimer.function=cadet_handler; | 
 | 		readtimer.data=(unsigned long)0; | 
 | 		readtimer.expires=jiffies+(HZ/20); | 
 | 		add_timer(&readtimer); | 
 | 	} | 
 | 	if(rdsin==rdsout) { | 
 |   	        if (file->f_flags & O_NONBLOCK) | 
 | 		        return -EWOULDBLOCK; | 
 | 	        interruptible_sleep_on(&read_queue); | 
 | 	}		 | 
 | 	while( i<count && rdsin!=rdsout) | 
 | 	        readbuf[i++]=rdsbuf[rdsout++]; | 
 |  | 
 | 	if (copy_to_user(data,readbuf,i)) | 
 | 	        return -EFAULT; | 
 | 	return i; | 
 | } | 
 |  | 
 |  | 
 |  | 
 | static int cadet_do_ioctl(struct inode *inode, struct file *file, | 
 | 			  unsigned int cmd, void *arg) | 
 | { | 
 | 	switch(cmd) | 
 | 	{ | 
 | 		case VIDIOCGCAP: | 
 | 		{ | 
 | 			struct video_capability *v = arg; | 
 | 			memset(v,0,sizeof(*v)); | 
 | 			v->type=VID_TYPE_TUNER; | 
 | 			v->channels=2; | 
 | 			v->audios=1; | 
 | 			strcpy(v->name, "ADS Cadet"); | 
 | 			return 0; | 
 | 		} | 
 | 		case VIDIOCGTUNER: | 
 | 		{ | 
 | 			struct video_tuner *v = arg; | 
 | 			if((v->tuner<0)||(v->tuner>1)) { | 
 | 				return -EINVAL; | 
 | 			} | 
 | 			switch(v->tuner) { | 
 | 			        case 0: | 
 | 			        strcpy(v->name,"FM"); | 
 | 			        v->rangelow=1400;     /* 87.5 MHz */ | 
 | 			        v->rangehigh=1728;    /* 108.0 MHz */ | 
 | 			        v->flags=0; | 
 | 			        v->mode=0; | 
 | 			        v->mode|=VIDEO_MODE_AUTO; | 
 | 			        v->signal=sigstrength; | 
 | 			        if(cadet_getstereo()==1) { | 
 | 				        v->flags|=VIDEO_TUNER_STEREO_ON; | 
 | 			        } | 
 | 				v->flags|=cadet_getrds(); | 
 | 			        break; | 
 | 			        case 1: | 
 | 			        strcpy(v->name,"AM"); | 
 | 			        v->rangelow=8320;      /* 520 kHz */ | 
 | 			        v->rangehigh=26400;    /* 1650 kHz */ | 
 | 			        v->flags=0; | 
 | 			        v->flags|=VIDEO_TUNER_LOW; | 
 | 			        v->mode=0; | 
 | 			        v->mode|=VIDEO_MODE_AUTO; | 
 | 			        v->signal=sigstrength; | 
 | 			        break; | 
 | 			} | 
 | 			return 0; | 
 | 		} | 
 | 		case VIDIOCSTUNER: | 
 | 		{ | 
 | 			struct video_tuner *v = arg; | 
 | 			if((v->tuner<0)||(v->tuner>1)) { | 
 | 				return -EINVAL; | 
 | 			} | 
 | 			curtuner=v->tuner;	 | 
 | 			return 0; | 
 | 		} | 
 | 		case VIDIOCGFREQ: | 
 | 		{ | 
 | 		        unsigned long *freq = arg; | 
 | 			*freq = cadet_getfreq(); | 
 | 			return 0; | 
 | 		} | 
 | 		case VIDIOCSFREQ: | 
 | 		{ | 
 | 		        unsigned long *freq = arg; | 
 | 			if((curtuner==0)&&((*freq<1400)||(*freq>1728))) { | 
 | 			        return -EINVAL; | 
 | 			} | 
 | 			if((curtuner==1)&&((*freq<8320)||(*freq>26400))) { | 
 | 			        return -EINVAL; | 
 | 			} | 
 | 			cadet_setfreq(*freq); | 
 | 			return 0; | 
 | 		} | 
 | 		case VIDIOCGAUDIO: | 
 | 		{	 | 
 | 			struct video_audio *v = arg; | 
 | 			memset(v,0, sizeof(*v)); | 
 | 			v->flags=VIDEO_AUDIO_MUTABLE|VIDEO_AUDIO_VOLUME; | 
 | 			if(cadet_getstereo()==0) { | 
 | 			        v->mode=VIDEO_SOUND_MONO; | 
 | 			} else { | 
 | 				v->mode=VIDEO_SOUND_STEREO; | 
 | 			} | 
 | 			v->volume=cadet_getvol(); | 
 | 			v->step=0xffff; | 
 | 			strcpy(v->name, "Radio"); | 
 | 			return 0;			 | 
 | 		} | 
 | 		case VIDIOCSAUDIO: | 
 | 		{ | 
 | 			struct video_audio *v = arg; | 
 | 			if(v->audio)  | 
 | 				return -EINVAL; | 
 | 			cadet_setvol(v->volume); | 
 | 			if(v->flags&VIDEO_AUDIO_MUTE)  | 
 | 				cadet_setvol(0); | 
 | 			else | 
 | 				cadet_setvol(0xffff); | 
 | 			return 0; | 
 | 		} | 
 | 		default: | 
 | 			return -ENOIOCTLCMD; | 
 | 	} | 
 | } | 
 |  | 
 | static int cadet_ioctl(struct inode *inode, struct file *file, | 
 | 		       unsigned int cmd, unsigned long arg) | 
 | { | 
 | 	return video_usercopy(inode, file, cmd, arg, cadet_do_ioctl); | 
 | } | 
 |  | 
 | static int cadet_open(struct inode *inode, struct file *file) | 
 | { | 
 | 	if(users) | 
 | 		return -EBUSY; | 
 | 	users++; | 
 | 	init_waitqueue_head(&read_queue); | 
 | 	return 0; | 
 | } | 
 |  | 
 | static int cadet_release(struct inode *inode, struct file *file) | 
 | { | 
 | 	del_timer_sync(&readtimer); | 
 | 	rdsstat=0; | 
 | 	users--; | 
 | 	return 0; | 
 | } | 
 |  | 
 |  | 
 | static struct file_operations cadet_fops = { | 
 | 	.owner		= THIS_MODULE, | 
 | 	.open		= cadet_open, | 
 | 	.release       	= cadet_release, | 
 | 	.read		= cadet_read, | 
 | 	.ioctl		= cadet_ioctl, | 
 | 	.llseek         = no_llseek, | 
 | }; | 
 |  | 
 | static struct video_device cadet_radio= | 
 | { | 
 | 	.owner		= THIS_MODULE, | 
 | 	.name		= "Cadet radio", | 
 | 	.type		= VID_TYPE_TUNER, | 
 | 	.hardware	= VID_HARDWARE_CADET, | 
 | 	.fops           = &cadet_fops, | 
 | }; | 
 |  | 
 | static struct pnp_device_id cadet_pnp_devices[] = { | 
 | 	/* ADS Cadet AM/FM Radio Card */ | 
 | 	{.id = "MSM0c24", .driver_data = 0}, | 
 | 	{.id = ""} | 
 | }; | 
 |  | 
 | MODULE_DEVICE_TABLE(pnp, cadet_pnp_devices); | 
 |  | 
 | static int cadet_pnp_probe(struct pnp_dev * dev, const struct pnp_device_id *dev_id) | 
 | { | 
 | 	if (!dev) | 
 | 		return -ENODEV; | 
 | 	/* only support one device */ | 
 | 	if (io > 0) | 
 | 		return -EBUSY; | 
 |  | 
 | 	if (!pnp_port_valid(dev, 0)) { | 
 | 		return -ENODEV; | 
 | 	} | 
 |  | 
 | 	io = pnp_port_start(dev, 0); | 
 |  | 
 | 	printk ("radio-cadet: PnP reports device at %#x\n", io); | 
 |  | 
 | 	return io; | 
 | } | 
 |  | 
 | static struct pnp_driver cadet_pnp_driver = { | 
 | 	.name		= "radio-cadet", | 
 | 	.id_table	= cadet_pnp_devices, | 
 | 	.probe		= cadet_pnp_probe, | 
 | 	.remove		= NULL, | 
 | }; | 
 |  | 
 | static int cadet_probe(void) | 
 | { | 
 |         static int iovals[8]={0x330,0x332,0x334,0x336,0x338,0x33a,0x33c,0x33e}; | 
 | 	int i; | 
 |  | 
 | 	for(i=0;i<8;i++) { | 
 | 	        io=iovals[i]; | 
 | 	        if(request_region(io,2, "cadet-probe")>=0) { | 
 | 		        cadet_setfreq(1410); | 
 | 			if(cadet_getfreq()==1410) { | 
 | 				release_region(io, 2); | 
 | 			        return io; | 
 | 			} | 
 | 			release_region(io, 2); | 
 | 		} | 
 | 	} | 
 | 	return -1; | 
 | } | 
 |  | 
 | /*  | 
 |  * io should only be set if the user has used something like | 
 |  * isapnp (the userspace program) to initialize this card for us | 
 |  */ | 
 |  | 
 | static int __init cadet_init(void) | 
 | { | 
 | 	spin_lock_init(&cadet_io_lock); | 
 | 	 | 
 | 	/* | 
 | 	 *	If a probe was requested then probe ISAPnP first (safest) | 
 | 	 */ | 
 | 	if (io < 0) | 
 | 		pnp_register_driver(&cadet_pnp_driver); | 
 | 	/* | 
 | 	 *	If that fails then probe unsafely if probe is requested | 
 | 	 */ | 
 | 	if(io < 0) | 
 | 		io = cadet_probe (); | 
 |  | 
 | 	/* | 
 | 	 *	Else we bail out | 
 | 	 */ | 
 | 	  | 
 |         if(io < 0) { | 
 | #ifdef MODULE         | 
 | 		printk(KERN_ERR "You must set an I/O address with io=0x???\n"); | 
 | #endif | 
 | 	        goto fail; | 
 | 	} | 
 | 	if (!request_region(io,2,"cadet")) | 
 | 		goto fail; | 
 | 	if(video_register_device(&cadet_radio,VFL_TYPE_RADIO,radio_nr)==-1) { | 
 | 		release_region(io,2); | 
 | 		goto fail; | 
 | 	} | 
 | 	printk(KERN_INFO "ADS Cadet Radio Card at 0x%x\n",io); | 
 | 	return 0; | 
 | fail: | 
 | 	pnp_unregister_driver(&cadet_pnp_driver); | 
 | 	return -1; | 
 | } | 
 |  | 
 |  | 
 |  | 
 | MODULE_AUTHOR("Fred Gleason, Russell Kroll, Quay Lu, Donald Song, Jason Lewis, Scott McGrath, William McGrath"); | 
 | MODULE_DESCRIPTION("A driver for the ADS Cadet AM/FM/RDS radio card."); | 
 | MODULE_LICENSE("GPL"); | 
 |  | 
 | module_param(io, int, 0); | 
 | MODULE_PARM_DESC(io, "I/O address of Cadet card (0x330,0x332,0x334,0x336,0x338,0x33a,0x33c,0x33e)"); | 
 | module_param(radio_nr, int, 0); | 
 |  | 
 | static void __exit cadet_cleanup_module(void) | 
 | { | 
 | 	video_unregister_device(&cadet_radio); | 
 | 	release_region(io,2); | 
 | 	pnp_unregister_driver(&cadet_pnp_driver); | 
 | } | 
 |  | 
 | module_init(cadet_init); | 
 | module_exit(cadet_cleanup_module); | 
 |  |