[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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.