[PATCH] V4L Driver for AVerMedia USB FM Radio
Faidon Liambotis <[email protected]>
| Newsgroups | gmane.linux.usb.devel,gmane.comp.video.video4linux |
|---|---|
| Message-ID | <[email protected]> |
Add a Video4Linux driver for AVerMedia's USB FM radio. Signed-off-by: Faidon Liambotis <[email protected]> --- drivers/hid/usbhid/hid-quirks.c | 4 + drivers/media/radio/Kconfig | 12 + drivers/media/radio/Makefile | 1 + drivers/media/radio/amusb.c | 538 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 555 insertions(+), 0 deletions(-) diff --git a/drivers/hid/usbhid/hid-quirks.c b/drivers/hid/usbhid/hid-quirks.c index 6b21a21..a16519d 100644 --- a/drivers/hid/usbhid/hid-quirks.c +++ b/drivers/hid/usbhid/hid-quirks.c @@ -343,6 +343,9 @@ #define USB_VENDOR_ID_YEALINK 0x6993 #define USB_DEVICE_ID_YEALINK_P1K_P4K_B2K 0xb001 +#define USB_VENDOR_ID_AVERMEDIA 0x07ca +#define USB_DEVICE_ID_AVERMEDIA_USBRADIO 0xb800 + /* * Alphabetically sorted blacklist by quirk type. */ @@ -597,6 +600,7 @@ static const struct hid_blacklist { { USB_VENDOR_ID_LOGITECH, USB_DEVICE_ID_LOGITECH_HARMONY_63, HID_QUIRK_IGNORE }, { USB_VENDOR_ID_LOGITECH, USB_DEVICE_ID_LOGITECH_HARMONY_64, HID_QUIRK_IGNORE }, { USB_VENDOR_ID_NATIONAL_SEMICONDUCTOR, USB_DEVICE_ID_N_S_HARMONY, HID_QUIRK_IGNORE }, + { USB_VENDOR_ID_AVERMEDIA, USB_DEVICE_ID_AVERMEDIA_USBRADIO, HID_QUIRK_IGNORE }, { 0, 0 } }; diff --git a/drivers/media/radio/Kconfig b/drivers/media/radio/Kconfig index f8bf9fe..28713dd 100644 --- a/drivers/media/radio/Kconfig +++ b/drivers/media/radio/Kconfig @@ -335,4 +335,16 @@ config USB_DSBR To compile this driver as a module, choose M here: the module will be called dsbr100. +config USB_AMUSB + tristate "AVerMedia USB FM radio support" + depends on USB && VIDEO_V4L2 + ---help--- + Say Y here if you want to connect this type of radio to your + computer's USB port. Note that the audio is not digital, and + you must connect the line out connector to a sound card or a + set of speakers. + + To compile this driver as a module, choose M here: the + module will be called amusb. + endif # RADIO_ADAPTERS diff --git a/drivers/media/radio/Makefile b/drivers/media/radio/Makefile index cf55a18..4158d9b 100644 --- a/drivers/media/radio/Makefile +++ b/drivers/media/radio/Makefile @@ -21,5 +21,6 @@ obj-$(CONFIG_RADIO_GEMTEK_PCI) += radio-gemtek-pci.o obj-$(CONFIG_RADIO_TRUST) += radio-trust.o obj-$(CONFIG_RADIO_MAESTRO) += radio-maestro.o obj-$(CONFIG_USB_DSBR) += dsbr100.o +obj-$(CONFIG_USB_AMUSB) += amusb.o EXTRA_CFLAGS += -Isound diff --git a/drivers/media/radio/amusb.c b/drivers/media/radio/amusb.c new file mode 100644 index 0000000..71628ee --- /dev/null +++ b/drivers/media/radio/amusb.c @@ -0,0 +1,538 @@ +/* AVerMedia USB FM radio Driver + * + * Copyright (c) 2007 Faidon Liambotis <[email protected]> + * Heavily based on D-Link DSB-R100's driver, + * Copyright (c) 2000 Markus Demleitner <[email protected]> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/slab.h> +#include <linux/input.h> +#include <linux/videodev2.h> +#include <media/v4l2-common.h> +#include <linux/usb.h> +#include <linux/smp_lock.h> + +/* + * Version Information + */ +#define DRIVER_AUTHOR "Faidon Liambotis <[email protected]>" +#define DRIVER_DESC "AVerMedia USB FM radio driver" + +static struct v4l2_queryctrl radio_qctrl[] = { + { + .id = V4L2_CID_AUDIO_MUTE, + .name = "Mute", + .minimum = 0, + .maximum = 1, + .default_value = 1, + .type = V4L2_CTRL_TYPE_BOOLEAN, + } +}; + +#define AMUSB_VENDOR 0x07ca +#define AMUSB_PRODUCT 0xb800 + +/* Device's commands */ + +/* unknown */ +/* unknown */ +#define AMUSB_FM_INIT 0xa3 +#define AMUSB_SET_FREQ 0xa4 +#define AMUSB_COMMAND_CHECK 0xa5 +/* unused */ +#define AMUSB_GET_SIGNAL 0xa7 +#define AMUSB_GET_FREQ 0xa8 +#define AMUSB_SET_SEARCH_UP 0xa9 +#define AMUSB_SET_SEARCH_DOWN 0xaa +#define AMUSB_SET_MUTE 0xab +#define AMUSB_SET_RIGHT_MUTE 0xac +#define AMUSB_SET_LEFT_MUTE 0xad +#define AMUSB_SET_MONO 0xae +/* unknown */ +#define AMUSB_SET_SEARCH_PARAM 0xb0 +#define AMUSB_STOP_AUTO_SEARCH 0xb1 + +/* search direction */ +#define AMUSB_SEARCH_DOWN 0x00 +#define AMUSB_SEARCH_UP 0x01 + +/* Frequency limits in MHz -- these are European values. For Japanese +devices, that would be 76 and 91. */ +#define FREQ_MIN 87.5 +#define FREQ_MAX 108.0 +#define FREQ_MUL 16000 + +/* tuner wants a 225Hz offset on frequency get/set */ +#define FREQ_MAGIC_OFFSET 225*16 + +static int usb_amusb_probe(struct usb_interface *intf, + const struct usb_device_id *id); +static void usb_amusb_disconnect(struct usb_interface *intf); +static int usb_amusb_ioctl(struct inode *inode, struct file *file, + unsigned int cmd, unsigned long arg); +static int usb_amusb_open(struct inode *inode, struct file *file); +static int usb_amusb_close(struct inode *inode, struct file *file); + +static int radio_nr = -1; +module_param(radio_nr, int, 0); + +/* Data for one (physical) device */ +struct amusb_device { + struct usb_device *usbdev; + struct video_device *videodev; + int curfreq; + int users; + bool removed; + bool muted; + bool stereo_user; + bool stereo_reception; + int signal; +}; + +/* File system interface */ +static const struct file_operations usb_amusb_fops = { + .owner = THIS_MODULE, + .open = usb_amusb_open, + .release = usb_amusb_close, + .ioctl = usb_amusb_ioctl, + .compat_ioctl = v4l_compat_ioctl32, + .llseek = no_llseek, +}; + +/* V4L interface */ +static struct video_device amusb_videodev_template = { + .owner = THIS_MODULE, + .name = "AverMedia USB FM radio", + .type = VID_TYPE_TUNER, + .fops = &usb_amusb_fops, + .release = video_device_release, +}; + +static struct usb_device_id usb_amusb_device_table [] = { + { USB_DEVICE(AMUSB_VENDOR, AMUSB_PRODUCT) }, + { } /* Terminating entry */ +}; + +MODULE_DEVICE_TABLE(usb, usb_amusb_device_table); + +/* USB subsystem interface */ +static struct usb_driver usb_amusb_driver = { + .name = "amusb", + .probe = usb_amusb_probe, + .disconnect = usb_amusb_disconnect, + .id_table = usb_amusb_device_table, +}; + +/* Low-level device interface begins here */ +static int amusb_command_raw(struct amusb_device *radio, char cmd, char arg, + int args, int extrabytes) +{ + int retv, wrotelen; + char b[8] = { + 0x00, 0x55, 0xaa, + args, + cmd, arg, + 0x00, + extrabytes + }; + + retv = usb_bulk_msg(radio->usbdev, usb_sndbulkpipe(radio->usbdev, 0x02), + b, sizeof(b), &wrotelen, 300); + if (retv < 0 || wrotelen < 8) + return retv; + + return wrotelen; +} + +static int amusb_command_short(struct amusb_device *radio, char cmd, char arg) +{ + return amusb_command_raw(radio, cmd, arg, 0, 0); +} + +static int amusb_command_long(struct amusb_device *radio, char cmd, char arg, + char *extrabuf, int buflen) +{ + int retv, wrotelen; + + retv = amusb_command_raw(radio, cmd, arg, buflen, 0x08); + if (retv < 0) + return retv; + + retv = usb_bulk_msg(radio->usbdev, usb_sndbulkpipe(radio->usbdev, 0x02), + extrabuf, buflen, &wrotelen, 300); + if (retv < 0) + return retv; + + return wrotelen; +} + +static int amusb_function(struct amusb_device *radio, char cmd, char arg, + char *ret, int retlen) +{ + int retv, readlen; + + retv = amusb_command_raw(radio, cmd, arg, 0, 8); + if (retv < 0) + return retv; + + retv = usb_bulk_msg(radio->usbdev, usb_rcvbulkpipe(radio->usbdev, 0x81), + ret, retlen, &readlen, 300); + if (retv < 0) + return retv; + + return readlen; +} + +/* + * V4L doesn't provide yet a method to do hardware-assisted searches + * Leave the function as a reverse-engineering documentation + */ +#if 0 +static int amusb_search(struct amusb_device *radio, int sensitivity, int direction) +{ + int retv; + char b[8] = { + 0x3d, 0x32, 0x0f, sensitivity, + 0x3d, 0x32, 0x0f, sensitivity + }; + + retv = amusb_command_long(radio, AMUSB_SET_SEARCH_PARAM, 0, b, 8); + if (retv < 0) + return retv; + + return amusb_command_short(radio, AMUSB_SET_SEARCH_DOWN, direction); +} +#endif + +static int amusb_set_mute(struct amusb_device *radio, bool mute) +{ + radio->muted = mute; + return amusb_command_short(radio, AMUSB_SET_MUTE, mute); +} + +static int amusb_set_stereo(struct amusb_device *radio, bool stereo) +{ + radio->stereo_user = stereo; + return amusb_command_short(radio, AMUSB_SET_MONO, !stereo); +} + +/* set a frequency, freq is defined by v4l's TUNER_LOW, i.e. 1/16th kHz */ +static int amusb_setfreq(struct amusb_device *radio, int freq) +{ + char b[3]; + + radio->curfreq = freq; + freq = (freq + FREQ_MAGIC_OFFSET) / 200; /* V4L -> tuner frequency */ + + b[0] = (freq >> 8) & 0xff; + b[1] = freq & 0xff; + + /* 1 means that we're adding magic offset, + * 0 that we're subtracting it */ + b[2] = 1; + + return amusb_command_long(radio, AMUSB_SET_FREQ, 0, b, 3); +} + +/* return the current frequency */ +static int amusb_getfreq(struct amusb_device *radio) +{ + int retv, f; + unsigned char b[8]; + retv = amusb_function(radio, AMUSB_GET_FREQ, 0, b, 8); + if (retv < 8) + return retv; + + f = b[1] * 0xff + b[2]; + f = f * 200; /* tuner->V4L2 */ + + if (b[3]) { + radio->curfreq = f - FREQ_MAGIC_OFFSET; + } else { + radio->curfreq = f + FREQ_MAGIC_OFFSET; + } + + return 0; +} + +/* return the tuner status, i.e. signal and stereo */ +static int amusb_getstat(struct amusb_device *radio) +{ + int retv; + unsigned char b[8]; + retv = amusb_function(radio, AMUSB_GET_SIGNAL, 0, b, 8); + if (retv < 8) + return retv; + + radio->stereo_reception = (b[2] >> 7); + radio->signal = (b[3] >> 4); + + return 0; +} + +/* check if the device is present and register with v4l and USB if it is */ +static int usb_amusb_probe(struct usb_interface *intf, + const struct usb_device_id *id) +{ + struct amusb_device *radio; + + radio = kmalloc(sizeof(struct amusb_device), GFP_KERNEL); + if (!radio) + return -ENOMEM; + + radio->videodev = video_device_alloc(); + if (!radio->videodev) { + kfree(radio); + return -ENOMEM; + } + + memcpy(radio->videodev, &amusb_videodev_template, + sizeof(amusb_videodev_template)); + radio->removed = 0; + radio->users = 0; + radio->usbdev = interface_to_usbdev(intf); + radio->curfreq = FREQ_MIN*FREQ_MUL; + video_set_drvdata(radio->videodev, radio); + + if (video_register_device(radio->videodev, VFL_TYPE_RADIO, radio_nr)) { + warn("Could not register video device"); + video_device_release(radio->videodev); + kfree(radio); + return -EIO; + } + usb_set_intfdata(intf, radio); + + return 0; +} + +/* handle unplugging of the device, release data structures +if nothing keeps us from doing it. If something is still +keeping us busy, the release callback of v4l will take care +of releasing it. stv680.c does not relase its private +data, so I don't do this here either. Checking out the +code I'd expect I better did that, but if there's a memory +leak here it's tiny (~50 bytes per disconnect) */ +static void usb_amusb_disconnect(struct usb_interface *intf) +{ + struct amusb_device *radio = usb_get_intfdata(intf); + + usb_set_intfdata(intf, NULL); + if (radio) { + video_unregister_device(radio->videodev); + radio->videodev = NULL; + if (radio->users) { + kfree(radio); + } else { + radio->removed = 1; + } + } +} + + +/* Video4Linux v2 interface */ +static int usb_amusb_do_ioctl(struct inode *inode, struct file *file, + unsigned int cmd, void *arg) +{ + struct amusb_device *radio = video_get_drvdata(video_devdata(file)); + + if (!radio) + return -EIO; + + switch (cmd) { + case VIDIOC_QUERYCAP: + { + struct v4l2_capability *v = arg; + memset(v, 0, sizeof(*v)); + strlcpy(v->driver, "amusb", sizeof(v->driver)); + strlcpy(v->card, "AVerMedia USB Radio", sizeof(v->card)); + sprintf(v->bus_info, "ISA"); + v->version = 1; + v->capabilities = V4L2_CAP_TUNER; + + return 0; + } + case VIDIOC_G_TUNER: + { + struct v4l2_tuner *v = arg; + + if (v->index > 0) + return -EINVAL; + + if (amusb_getstat(radio) < 0) + warn("Reading signal failed"); + + memset(v, 0, sizeof(*v)); + strcpy(v->name, "FM"); + v->type = V4L2_TUNER_RADIO; + + v->rangelow = FREQ_MIN*FREQ_MUL; + v->rangehigh = FREQ_MAX*FREQ_MUL; + + /* use 62.5Hz steps when tuning frequencies */ + v->capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO; + + if (radio->stereo_reception) { + v->rxsubchans = V4L2_TUNER_SUB_STEREO; + } else { + v->rxsubchans = V4L2_TUNER_SUB_MONO; + } + + if (radio->stereo_reception && radio->stereo_user) { + v->audmode = V4L2_TUNER_MODE_STEREO; + } else { + v->audmode = V4L2_TUNER_MODE_MONO; + } + + /* device reports signal 0x0 - 0xf, scale it to V4L */ + v->signal = radio->signal << 12; + + return 0; + } + case VIDIOC_S_TUNER: + { + struct v4l2_tuner *v = arg; + bool wantstereo; + + if (v->index > 0) + return -EINVAL; + + wantstereo = (v->audmode == V4L2_TUNER_MODE_STEREO); + if (amusb_set_stereo(radio, wantstereo) < 0) + warn("Set Stereo mode failed"); + return 0; + } + case VIDIOC_S_FREQUENCY: + { + struct v4l2_frequency *f = arg; + + if (amusb_setfreq(radio, f->frequency) < 0) + warn("Set frequency failed"); + return 0; + } + case VIDIOC_G_FREQUENCY: + { + struct v4l2_frequency *f = arg; + + f->type = V4L2_TUNER_RADIO; + if (amusb_getfreq(radio) < 0) + warn("Get frequency failed"); + f->frequency = radio->curfreq; + + return 0; + } + case VIDIOC_QUERYCTRL: + { + struct v4l2_queryctrl *qc = arg; + int i; + + for (i = 0; i < ARRAY_SIZE(radio_qctrl); i++) { + if (qc->id && qc->id == radio_qctrl[i].id) { + memcpy(qc, &(radio_qctrl[i]), + sizeof(*qc)); + return 0; + } + } + return -EINVAL; + } + case VIDIOC_G_CTRL: + { + struct v4l2_control *ctrl = arg; + + switch (ctrl->id) { + case V4L2_CID_AUDIO_MUTE: + ctrl->value = radio->muted; + return 0; + } + return -EINVAL; + } + case VIDIOC_S_CTRL: + { + struct v4l2_control *ctrl = arg; + + switch (ctrl->id) { + case V4L2_CID_AUDIO_MUTE: + if (amusb_set_mute(radio, ctrl->value) < 0) + warn("Radio did not respond properly"); + return 0; + } + return -EINVAL; + } + default: + return v4l_compat_translate_ioctl(inode, file, cmd, arg, + usb_amusb_do_ioctl); + } +} + +static int usb_amusb_ioctl(struct inode *inode, struct file *file, + unsigned int cmd, unsigned long arg) +{ + return video_usercopy(inode, file, cmd, arg, usb_amusb_do_ioctl); +} + +static int usb_amusb_open(struct inode *inode, struct file *file) +{ + struct amusb_device *radio = video_get_drvdata(video_devdata(file)); + + radio->users = 1; + + /* check if the radio works by muting it */ + if (amusb_set_mute(radio, 1) < 0) { + warn("Radio did not start up properly"); + radio->users = 0; + return -EIO; + } + + /* enable stereo mode */ + amusb_set_stereo(radio, 1); + + amusb_setfreq(radio, radio->curfreq); + return 0; +} + +static int usb_amusb_close(struct inode *inode, struct file *file) +{ + struct amusb_device *radio = video_get_drvdata(video_devdata(file)); + + if (!radio) + return -ENODEV; + radio->users = 0; + if (radio->removed) + kfree(radio); + + return 0; +} + +static int __init amusb_init(void) +{ + int retval = usb_register(&usb_amusb_driver); + info(DRIVER_DESC); + return retval; +} + +static void __exit amusb_exit(void) +{ + usb_deregister(&usb_amusb_driver); +} + +module_init(amusb_init); +module_exit(amusb_exit); + +MODULE_AUTHOR(DRIVER_AUTHOR); +MODULE_DESCRIPTION(DRIVER_DESC); +MODULE_LICENSE("GPL"); -- 1.5.3.2 ------------------------------------------------------------------------- This SF.net email is sponsored by: Splunk Inc. Still grepping through log files to find problems? Stop. Now Search log events and configuration files using AJAX and a browser. Download your FREE copy of Splunk now >> http://get.splunk.com/ _______________________________________________ [email protected] To unsubscribe, use the last form field at: https://lists.sourceforge.net/lists/listinfo/linux-usb-devel