2005-04-17 02:20:36 +04:00
/* 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 */
2005-09-13 12:25:15 +04:00
# include <linux/ioport.h> /* request_region */
2005-04-17 02:20:36 +04:00
# 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 ,
2006-01-09 20:24:57 +03:00
. compat_ioctl = v4l_compat_ioctl32 ,
2005-04-17 02:20:36 +04:00
. 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 ] ;
2005-10-15 02:59:04 +04:00
if ( request_region ( io , 2 , " cadet-probe " ) ) {
2005-04-17 02:20:36 +04:00
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 ) ;