aboutsummaryrefslogtreecommitdiff
path: root/drivers/media/radio/radio-cadet.c
diff options
context:
space:
mode:
Diffstat (limited to 'drivers/media/radio/radio-cadet.c')
-rw-r--r--drivers/media/radio/radio-cadet.c620
1 files changed, 620 insertions, 0 deletions
diff --git a/drivers/media/radio/radio-cadet.c b/drivers/media/radio/radio-cadet.c
new file mode 100644
index 00000000000..53d399b6652
--- /dev/null
+++ b/drivers/media/radio/radio-cadet.c
@@ -0,0 +1,620 @@
+/* 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);
+