diff options
Diffstat (limited to 'drivers/media/radio/radio-cadet.c')
-rw-r--r-- | drivers/media/radio/radio-cadet.c | 620 |
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); + |