[PATCH 17/33] add tea5761 radio driver

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



From: Felipe Balbi <felipe.balbi@xxxxxxxxx>

Signed-off-by: Felipe Balbi <felipe.balbi@xxxxxxxxx>
---
 drivers/media/radio/Kconfig         |   13 +
 drivers/media/radio/Makefile        |    1 +
 drivers/media/radio/radio-tea5761.c |  517 +++++++++++++++++++++++++++++++++++
 3 files changed, 531 insertions(+), 0 deletions(-)
 create mode 100644 drivers/media/radio/radio-tea5761.c

diff --git a/drivers/media/radio/Kconfig b/drivers/media/radio/Kconfig
index 1b41b3f..de6ca27 100644
--- a/drivers/media/radio/Kconfig
+++ b/drivers/media/radio/Kconfig
@@ -339,6 +339,19 @@ config RADIO_ZOLTRIX_PORT
 	help
 	  Enter the I/O port of your Zoltrix radio card.
 
+config RADIO_TEA5761
+	tristate "Philips Semiconductors TEA5761 I2C FM Radio"
+	help
+	  Choose Y here if you have one of these AM/FM radio cards.
+
+	  In order to control your radio card, you will need to use programs
+	  that are compatible with the Video For Linux 2 API.  Information on
+	  this API and pointers to "v4l" programs may be found at
+	  <file:Documentation/video4linux/API.html>.
+
+	  To compile this driver as a module, choose M here: the
+	  module will be called radio-tea5761.
+
 config USB_DSBR
 	tristate "D-Link/GemTek USB FM radio support"
 	depends on USB && VIDEO_V4L2
diff --git a/drivers/media/radio/Makefile b/drivers/media/radio/Makefile
index a30159f..f5bffcc 100644
--- a/drivers/media/radio/Makefile
+++ b/drivers/media/radio/Makefile
@@ -20,6 +20,7 @@ obj-$(CONFIG_RADIO_GEMTEK) += radio-gemtek.o
 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_RADIO_TEA5761) += radio-tea5761.o
 obj-$(CONFIG_USB_DSBR) += dsbr100.o
 obj-$(CONFIG_USB_SI470X) += radio-si470x.o
 
diff --git a/drivers/media/radio/radio-tea5761.c b/drivers/media/radio/radio-tea5761.c
new file mode 100644
index 0000000..1c6d8ab
--- /dev/null
+++ b/drivers/media/radio/radio-tea5761.c
@@ -0,0 +1,517 @@
+/*
+ * drivers/media/radio/radio-tea5761.c
+ *
+ * Copyright (C) 2005-2008 Nokia Corporation
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+ */
+#include <linux/module.h>
+#include <linux/version.h>
+#include <linux/init.h>
+#include <linux/i2c.h>
+#include <linux/delay.h>
+#include <media/v4l2-common.h>
+#include <media/v4l2-ioctl.h>
+
+#define DRIVER_NAME "tea5761"
+
+#define TEA5761_VERSION		KERNEL_VERSION(0, 0, 1)
+
+#define TEA5761_I2C_ADDR	0x10
+
+#define TEA5761_MANID		0x002b
+#define TEA5761_CHIPID		0x5761
+
+#define TEA5761_INTREG_BLMSK	0x0001
+#define TEA5761_INTREG_FRRMSK	0x0002
+#define TEA5761_INTREG_LEVMSK	0x0008
+#define TEA5761_INTREG_IFMSK	0x0010
+#define TEA5761_INTREG_BLMFLAG	0x0100
+#define TEA5761_INTREG_FRRFLAG	0x0200
+#define TEA5761_INTREG_LEVFLAG	0x0800
+#define TEA5761_INTREG_IFFLAG	0x1000
+
+#define TEA5761_FRQSET_SUD	0x8000
+#define TEA5761_FRQSET_SM	0x4000
+
+#define TEA5761_TNCTRL_PUPD0	0x4000
+#define TEA5761_TNCTRL_BLIM	0x2000
+#define TEA5761_TNCTRL_SWPM	0x1000
+#define TEA5761_TNCTRL_IFCTC	0x0800
+#define TEA5761_TNCTRL_AFM	0x0400
+#define TEA5761_TNCTRL_SMUTE	0x0200
+#define TEA5761_TNCTRL_SNC	0x0100
+#define TEA5761_TNCTRL_MU	0x0080
+#define TEA5761_TNCTRL_SSL1	0x0040
+#define TEA5761_TNCTRL_SSL0	0x0020
+#define TEA5761_TNCTRL_HLSI	0x0010
+#define TEA5761_TNCTRL_MST	0x0008
+#define TEA5761_TNCTRL_SWP	0x0004
+#define TEA5761_TNCTRL_DTC	0x0002
+#define TEA5761_TNCTRL_AHLSI	0x0001
+
+#define TEA5761_TUNCHK_LEVEL(x)	(((x) & 0x00F0) >> 4)
+#define TEA5761_TUNCHK_IFCNT(x) (((x) & 0xFE00) >> 9)
+#define TEA5761_TUNCHK_TUNTO	0x0100
+#define TEA5761_TUNCHK_LD	0x0008
+#define TEA5761_TUNCHK_STEREO	0x0004
+
+#define TEA5761_TESTREG_TRIGFR	0x0800
+
+#define TEA5761_FREQ_LOW	87500
+#define TEA5761_FREQ_HIGH	108000
+
+struct tea5761_regs {
+	u16 intreg;
+	u16 frqset;
+	u16 tnctrl;
+	u16 frqchk;
+	u16 tunchk;
+	u16 testreg;
+	u16 manid;
+	u16 chipid;
+} __attribute__ ((packed));
+
+struct tea5761_write_regs {
+	u8 intreg;
+	u16 frqset;
+	u16 tnctrl;
+	u16 testreg;
+} __attribute__ ((packed));
+
+struct tea5761_device {
+	struct video_device	*video_dev;
+	struct i2c_client	*i2c_dev;
+	struct tea5761_regs	regs;
+	struct mutex		mutex;
+	int			users;
+};
+
+static struct tea5761_device tea5761;
+
+static struct i2c_driver	tea5761_driver;
+static int radio_nr = -1;
+
+static int tea5761_read_regs(struct tea5761_device *tea)
+{
+	int rc, i;
+	u16 *p = (u16 *) &tea->regs;
+	struct i2c_client *client = tea->i2c_dev;
+
+	rc = i2c_master_recv(client, (void*) &tea->regs, sizeof(tea->regs));
+	for (i = 0; i < 8; i++) {
+		p[i] = __be16_to_cpu(p[i]);
+	}
+
+	dev_dbg(&client->dev,
+		"chip state: %04x %04x %04x %04x %04x %04x %04x %04x\n",
+		p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7]);
+
+	if (rc < 0)
+		dev_err(&client->dev, "read\n");
+
+	return rc;
+}
+
+static void tea5761_write_regs(struct tea5761_device *tea)
+{
+	struct tea5761_write_regs wr;
+	struct tea5761_regs *r = &tea->regs;
+	struct i2c_client *client = tea->i2c_dev;
+	u8 *p = (u8 *) r;
+
+	wr.intreg = r->intreg & 0xff;
+	wr.frqset = __cpu_to_be16(r->frqset);
+	wr.tnctrl = __cpu_to_be16(r->tnctrl);
+	wr.testreg = __cpu_to_be16(r->testreg);
+
+	dev_dbg(&client->dev,
+		"writing state: %02x %02x %02x %02x %02x %02x %02x\n",
+		p[0], p[1], p[2], p[3], p[4], p[5], p[6]);
+	if (i2c_master_send(client, (void *) &wr, sizeof(wr)) < 0)
+		dev_err(&client->dev, "write\n");
+}
+
+static void tea5761_power_up(struct tea5761_device *tea)
+{
+	struct tea5761_regs *r = &tea->regs;
+
+	if (!(r->tnctrl & TEA5761_TNCTRL_PUPD0)) {
+		r->tnctrl &= ~(TEA5761_TNCTRL_AFM | TEA5761_TNCTRL_MU |
+			       TEA5761_TNCTRL_HLSI);
+		r->testreg |= TEA5761_TESTREG_TRIGFR;
+		r->tnctrl |= TEA5761_TNCTRL_PUPD0;
+		return tea5761_write_regs(tea);
+	}
+}
+
+static void tea5761_power_down(struct tea5761_device *tea)
+{
+	struct tea5761_regs *r = &tea->regs;
+
+	if (r->tnctrl & TEA5761_TNCTRL_PUPD0) {
+		r->tnctrl &= ~TEA5761_TNCTRL_PUPD0;
+		return tea5761_write_regs(tea);
+	}
+}
+
+static void tea5761_set_freq(struct tea5761_device *tea, int freq)
+{
+	struct tea5761_regs *r = &tea->regs;
+
+	if (r->tnctrl & TEA5761_TNCTRL_HLSI)
+		r->frqset = (freq + 225000) / 8192;
+	else
+		r->frqset = (freq - 225000) / 8192;
+}
+
+static int tea5761_get_freq(struct tea5761_device *tea)
+{
+	struct tea5761_regs *r = &tea->regs;
+
+	if (r->tnctrl & TEA5761_TNCTRL_HLSI)
+		return (r->frqchk * 8192) - 225000;
+	else
+		return (r->frqchk * 8192) + 225000;
+}
+
+static void tea5761_tune(struct tea5761_device *tea, int freq)
+{
+	tea5761_set_freq(tea, freq);
+	tea5761_write_regs(tea);
+}
+
+static void tea5761_set_audout_mode(struct tea5761_device *tea, int audmode)
+{
+	struct tea5761_regs *r = &tea->regs;
+	int tnctrl = r->tnctrl;
+
+	if (audmode == V4L2_TUNER_MODE_MONO)
+		r->tnctrl |= TEA5761_TNCTRL_MST;
+	else
+		r->tnctrl &= ~TEA5761_TNCTRL_MST;
+	if (tnctrl != r->tnctrl)
+		tea5761_write_regs(tea);
+}
+
+static int tea5761_get_audout_mode(struct tea5761_device *tea)
+{
+	struct tea5761_regs *r = &tea->regs;
+
+	if (r->tnctrl & TEA5761_TNCTRL_MST)
+		return V4L2_TUNER_MODE_MONO;
+	else
+		return V4L2_TUNER_MODE_STEREO;
+}
+
+static void tea5761_mute(struct tea5761_device *tea, int on)
+{
+	struct tea5761_regs *r = &tea->regs;
+	int tnctrl = r->tnctrl;
+
+	if (on)
+		r->tnctrl |= TEA5761_TNCTRL_MU;
+	else
+		r->tnctrl &= ~TEA5761_TNCTRL_MU;
+	if (tnctrl != r->tnctrl)
+		tea5761_write_regs(tea);
+}
+
+static int tea5761_is_muted(struct tea5761_device *tea)
+{
+	return tea->regs.tnctrl & TEA5761_TNCTRL_MU;
+}
+
+static int tea5761_do_ioctl(struct inode *inode, struct file *file,
+			    unsigned int cmd, void *arg)
+{
+	struct tea5761_device *tea = file->private_data;
+	struct video_device *dev = tea->video_dev;
+	struct i2c_client *client = tea->i2c_dev;
+	struct tea5761_regs *r = &tea->regs;
+
+	union {
+		struct v4l2_capability c;
+		struct v4l2_tuner t;
+		struct v4l2_frequency f;
+		struct v4l2_queryctrl qc;
+		struct v4l2_control ct;
+	} *u = arg;
+
+	tea5761_read_regs(tea);
+
+	switch (cmd) {
+	case VIDIOC_QUERYCAP:
+		dev_dbg(&client->dev, "VIDIOC_QUERYCAP\n");
+		memset(&u->c, 0, sizeof(u->c));
+		strlcpy(u->c.driver,
+			dev->parent->driver->name,
+			sizeof(u->c.driver));
+		strlcpy(u->c.card, dev->name, sizeof(u->c.card));
+		snprintf(u->c.bus_info, sizeof(u->c.bus_info), "I2C:%s",
+			 dev->parent->bus_id);
+		u->c.version = TEA5761_VERSION;
+		u->c.capabilities = V4L2_CAP_TUNER | V4L2_CAP_RADIO;
+		break;
+
+	case VIDIOC_G_TUNER:
+		/* Only one tuner chip */
+		dev_dbg(&client->dev, "VIDIOC_G_TUNER\n");
+		if (u->t.index != 0)
+			return -EINVAL;
+
+		memset(&u->t, 0, sizeof(u->t));
+		u->t.type = V4L2_TUNER_RADIO;
+		strlcpy(u->t.name, "FM", sizeof(u->t.name));
+		/* Freq in 62.5Hz units */
+		u->t.rangelow = TEA5761_FREQ_LOW * 16;
+		u->t.rangehigh = TEA5761_FREQ_HIGH * 16;
+		u->t.capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO;
+		if (r->tunchk & TEA5761_TUNCHK_STEREO)
+			u->t.rxsubchans = V4L2_TUNER_SUB_STEREO;
+		u->t.audmode = tea5761_get_audout_mode(tea);
+		u->t.signal = TEA5761_TUNCHK_LEVEL(r->tunchk) * 0xffff / 0xf;
+		u->t.afc = TEA5761_TUNCHK_IFCNT(r->tunchk);
+		break;
+
+	case VIDIOC_S_TUNER:
+		/* Only tuner nro 0 can be selected. */
+		dev_dbg(&client->dev, "VIDIOC_S_TUNER\n");
+		if (u->t.index != 0)
+			return -EINVAL;
+		tea5761_set_audout_mode(tea, u->t.audmode);
+		break;
+
+	case VIDIOC_G_FREQUENCY:
+		dev_dbg(&client->dev, "VIDIOC_G_FREQUENCY\n");
+		memset(&u->f, 0, sizeof(u->f));
+		u->f.type = V4L2_TUNER_RADIO;
+		if (r->tnctrl & TEA5761_TNCTRL_PUPD0)
+			u->f.frequency = (tea5761_get_freq(tea) * 2) / 125;
+		else
+			u->f.frequency = 0;
+		break;
+
+	case VIDIOC_S_FREQUENCY:
+		dev_dbg(&client->dev, "VIDIOC_S_FREQUENCY %u\n",
+			u->f.frequency);
+		if (u->f.tuner != 0)
+			return -EINVAL;
+		if (u->f.frequency == 0) {
+			/* We special case this as a power down
+			 * control. */
+			tea5761_power_down(tea);
+			break;
+		}
+		if (u->f.frequency < 16 * TEA5761_FREQ_LOW)
+			return -EINVAL;
+		if (u->f.frequency > 16 * TEA5761_FREQ_HIGH)
+			return -EINVAL;
+
+		tea5761_power_up(tea);
+		tea5761_tune(tea, (u->f.frequency * 125) / 2);
+		break;
+
+	case VIDIOC_QUERYCTRL:
+		dev_dbg(&client->dev, "VIDIOC_QUERYCTRL %d\n", u->qc.id);
+		if (u->qc.id != V4L2_CID_AUDIO_MUTE)
+			return -EINVAL;
+		strlcpy(u->qc.name, "Mute", sizeof(u->qc.name));
+		u->qc.minimum = 0;
+		u->qc.maximum = 1;
+		u->qc.step = 1;
+		u->qc.default_value = 0;
+		u->qc.type = V4L2_CTRL_TYPE_BOOLEAN;
+		break;
+
+	case VIDIOC_G_CTRL:
+		dev_dbg(&client->dev, "VIDIOC_G_CTRL %d\n", u->ct.id);
+		if (u->ct.id != V4L2_CID_AUDIO_MUTE)
+			return -EINVAL;
+		if (r->tnctrl & TEA5761_TNCTRL_PUPD0)
+			u->ct.value = tea5761_is_muted(tea) ? 1 : 0;
+		else
+			u->ct.value = 0;
+		break;
+
+	case VIDIOC_S_CTRL:
+		dev_dbg(&client->dev, "VIDIOC_S_CTRL %d\n", u->ct.id);
+		if (u->ct.id != V4L2_CID_AUDIO_MUTE)
+			return -EINVAL;
+		tea5761_mute(tea, u->ct.value);
+		break;
+
+	default:
+		return -ENOIOCTLCMD;
+	}
+
+	return 0;
+}
+
+static int tea5761_ioctl(struct inode *inode, struct file *file,
+			 unsigned int cmd, unsigned long arg)
+{
+	return video_usercopy(inode, file, cmd, arg, tea5761_do_ioctl);
+}
+
+static int tea5761_open(struct inode *inode, struct file *file)
+{
+	int minor = iminor(file->f_dentry->d_inode);
+	/* Currently we support only one device */
+	struct tea5761_device *tea = &tea5761;
+
+	if (tea->video_dev->minor != minor)
+		return -ENODEV;
+
+	mutex_lock(&tea->mutex);
+	/* Only exclusive access */
+	if (tea->users) {
+		mutex_unlock(&tea->mutex);
+		return -EBUSY;
+	}
+	tea->users++;
+	mutex_unlock(&tea->mutex);
+
+	file->private_data = tea;
+	return 0;
+}
+
+static int tea5761_release(struct inode *inode, struct file *file)
+{
+	struct tea5761_device *tea = file->private_data;
+
+	mutex_lock(&tea->mutex);
+	tea->users--;
+	mutex_unlock(&tea->mutex);
+
+	return 0;
+}
+
+static struct file_operations tea5761_fops = {
+	.owner		= THIS_MODULE,
+	.open           = tea5761_open,
+	.release	= tea5761_release,
+	.ioctl		= tea5761_ioctl,
+	.llseek         = no_llseek,
+};
+
+static struct video_device tea5761_video_device = {
+	.name          = "TEA5761 FM-Radio",
+	.vfl_type      = VID_TYPE_TUNER,
+	.fops          = &tea5761_fops,
+	.release       = video_device_release
+};
+
+static int tea5761_i2c_driver_probe(struct i2c_client *client,
+		const struct i2c_device_id *id)
+{
+	struct video_device *video_dev;
+	int err = 0;
+	struct tea5761_device *tea = &tea5761;
+
+	mutex_init(&tea->mutex);
+
+	tea->i2c_dev = client;
+
+	/* V4L initialization */
+	video_dev = video_device_alloc();
+	if (video_dev == NULL) {
+		dev_err(&client->dev, "couldn't allocate memory\n");
+		err = -ENOMEM;
+		goto exit;
+	}
+	tea->video_dev = video_dev;
+
+	*video_dev = tea5761_video_device;
+	video_dev->parent = &client->dev;
+	i2c_set_clientdata(client, video_dev);
+
+	/* initialize and power off the chip */
+	tea5761_read_regs(tea);
+	tea5761_set_audout_mode(tea, V4L2_TUNER_MODE_STEREO);
+	tea5761_mute(tea, 0);
+	tea5761_power_down(tea);
+
+	tea5761.video_dev = video_dev;
+	tea5761.i2c_dev = client;
+
+	err = video_register_device(video_dev, VFL_TYPE_RADIO, radio_nr);
+	if (err) {
+		dev_err(&client->dev, "couldn't register video device\n");
+		goto err_video_alloc;
+	}
+
+	dev_info(&client->dev, "tea5761 (version %d) detected\n",
+		(tea->regs.manid >> 12) & 0xf);
+
+	return 0;
+
+err_video_alloc:
+	video_device_release(video_dev);
+exit:
+	kfree(client);
+	return err;
+}
+
+static int tea5761_i2c_driver_remove(struct i2c_client *client)
+{
+	struct video_device *vd = i2c_get_clientdata(client);
+
+	video_unregister_device(vd);
+
+	return 0;
+}
+
+static const struct i2c_device_id tea5761_id[] = {
+	{ DRIVER_NAME, 0 },
+	{ },
+};
+MODULE_DEVICE_TABLE(i2c, tea5761_id);
+
+static struct i2c_driver tea5761_driver = {
+	.driver = {
+		.name	= DRIVER_NAME,
+	},
+	.probe	= tea5761_i2c_driver_probe,
+	.remove = __devexit_p(tea5761_i2c_driver_remove),
+	.id_table = tea5761_id,
+};
+
+static int __init tea5761_init(void)
+{
+	int res;
+
+	if ((res = i2c_add_driver(&tea5761_driver))) {
+		printk(KERN_ERR DRIVER_NAME ": driver registration failed\n");
+		return res;
+	}
+
+	return 0;
+}
+
+static void __exit tea5761_exit(void)
+{
+	i2c_del_driver(&tea5761_driver);
+}
+
+MODULE_AUTHOR("Timo Teräs");
+MODULE_DESCRIPTION("I2C interface for TEA5761.");
+MODULE_LICENSE("GPL");
+
+module_param(radio_nr, int, 0);
+MODULE_PARM_DESC(nr_radio, "video4linux device number to use");
+
+module_init(tea5761_init)
+module_exit(tea5761_exit)
-- 
1.6.0.1.141.g445ca

--
To unsubscribe from this list: send the line "unsubscribe linux-omap" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html

[Index of Archives]     [Linux Arm (vger)]     [ARM Kernel]     [ARM MSM]     [Linux Tegra]     [Linux WPAN Networking]     [Linux Wireless Networking]     [Maemo Users]     [Linux USB Devel]     [Video for Linux]     [Linux Audio Users]     [Yosemite Trails]     [Linux Kernel]     [Linux SCSI]

  Powered by Linux