summaryrefslogtreecommitdiffstats
path: root/drivers/media/radio/radio-ma901.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 10:05:51 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 10:05:51 +0000
commit5d1646d90e1f2cceb9f0828f4b28318cd0ec7744 (patch)
treea94efe259b9009378be6d90eb30d2b019d95c194 /drivers/media/radio/radio-ma901.c
parentInitial commit. (diff)
downloadlinux-5d1646d90e1f2cceb9f0828f4b28318cd0ec7744.tar.xz
linux-5d1646d90e1f2cceb9f0828f4b28318cd0ec7744.zip
Adding upstream version 5.10.209.upstream/5.10.209
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'drivers/media/radio/radio-ma901.c')
-rw-r--r--drivers/media/radio/radio-ma901.c456
1 files changed, 456 insertions, 0 deletions
diff --git a/drivers/media/radio/radio-ma901.c b/drivers/media/radio/radio-ma901.c
new file mode 100644
index 000000000..657c3dda6
--- /dev/null
+++ b/drivers/media/radio/radio-ma901.c
@@ -0,0 +1,456 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Driver for the MasterKit MA901 USB FM radio. This device plugs
+ * into the USB port and an analog audio input or headphones, so this thing
+ * only deals with initialization, frequency setting, volume.
+ *
+ * Copyright (c) 2012 Alexey Klimov <klimov.linux@gmail.com>
+ */
+
+#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-device.h>
+#include <media/v4l2-ioctl.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-event.h>
+#include <linux/usb.h>
+#include <linux/mutex.h>
+
+#define DRIVER_AUTHOR "Alexey Klimov <klimov.linux@gmail.com>"
+#define DRIVER_DESC "Masterkit MA901 USB FM radio driver"
+#define DRIVER_VERSION "0.0.1"
+
+MODULE_AUTHOR(DRIVER_AUTHOR);
+MODULE_DESCRIPTION(DRIVER_DESC);
+MODULE_LICENSE("GPL");
+MODULE_VERSION(DRIVER_VERSION);
+
+#define USB_MA901_VENDOR 0x16c0
+#define USB_MA901_PRODUCT 0x05df
+
+/* dev_warn macro with driver name */
+#define MA901_DRIVER_NAME "radio-ma901"
+#define ma901radio_dev_warn(dev, fmt, arg...) \
+ dev_warn(dev, MA901_DRIVER_NAME " - " fmt, ##arg)
+
+#define ma901radio_dev_err(dev, fmt, arg...) \
+ dev_err(dev, MA901_DRIVER_NAME " - " fmt, ##arg)
+
+/* Probably USB_TIMEOUT should be modified in module parameter */
+#define BUFFER_LENGTH 8
+#define USB_TIMEOUT 500
+
+#define FREQ_MIN 87.5
+#define FREQ_MAX 108.0
+#define FREQ_MUL 16000
+
+#define MA901_VOLUME_MAX 16
+#define MA901_VOLUME_MIN 0
+
+/* Commands that device should understand
+ * List isn't full and will be updated with implementation of new functions
+ */
+#define MA901_RADIO_SET_FREQ 0x03
+#define MA901_RADIO_SET_VOLUME 0x04
+#define MA901_RADIO_SET_MONO_STEREO 0x05
+
+/* Comfortable defines for ma901radio_set_stereo */
+#define MA901_WANT_STEREO 0x50
+#define MA901_WANT_MONO 0xd0
+
+/* module parameter */
+static int radio_nr = -1;
+module_param(radio_nr, int, 0);
+MODULE_PARM_DESC(radio_nr, "Radio file number");
+
+/* Data for one (physical) device */
+struct ma901radio_device {
+ /* reference to USB and video device */
+ struct usb_device *usbdev;
+ struct usb_interface *intf;
+ struct video_device vdev;
+ struct v4l2_device v4l2_dev;
+ struct v4l2_ctrl_handler hdl;
+
+ u8 *buffer;
+ struct mutex lock; /* buffer locking */
+ int curfreq;
+ u16 volume;
+ int stereo;
+ bool muted;
+};
+
+static inline struct ma901radio_device *to_ma901radio_dev(struct v4l2_device *v4l2_dev)
+{
+ return container_of(v4l2_dev, struct ma901radio_device, v4l2_dev);
+}
+
+/* set a frequency, freq is defined by v4l's TUNER_LOW, i.e. 1/16th kHz */
+static int ma901radio_set_freq(struct ma901radio_device *radio, int freq)
+{
+ unsigned int freq_send = 0x300 + (freq >> 5) / 25;
+ int retval;
+
+ radio->buffer[0] = 0x0a;
+ radio->buffer[1] = MA901_RADIO_SET_FREQ;
+ radio->buffer[2] = ((freq_send >> 8) & 0xff) + 0x80;
+ radio->buffer[3] = freq_send & 0xff;
+ radio->buffer[4] = 0x00;
+ radio->buffer[5] = 0x00;
+ radio->buffer[6] = 0x00;
+ radio->buffer[7] = 0x00;
+
+ retval = usb_control_msg(radio->usbdev, usb_sndctrlpipe(radio->usbdev, 0),
+ 9, 0x21, 0x0300, 0,
+ radio->buffer, BUFFER_LENGTH, USB_TIMEOUT);
+ if (retval < 0)
+ return retval;
+
+ radio->curfreq = freq;
+ return 0;
+}
+
+static int ma901radio_set_volume(struct ma901radio_device *radio, u16 vol_to_set)
+{
+ int retval;
+
+ radio->buffer[0] = 0x0a;
+ radio->buffer[1] = MA901_RADIO_SET_VOLUME;
+ radio->buffer[2] = 0xc2;
+ radio->buffer[3] = vol_to_set + 0x20;
+ radio->buffer[4] = 0x00;
+ radio->buffer[5] = 0x00;
+ radio->buffer[6] = 0x00;
+ radio->buffer[7] = 0x00;
+
+ retval = usb_control_msg(radio->usbdev, usb_sndctrlpipe(radio->usbdev, 0),
+ 9, 0x21, 0x0300, 0,
+ radio->buffer, BUFFER_LENGTH, USB_TIMEOUT);
+ if (retval < 0)
+ return retval;
+
+ radio->volume = vol_to_set;
+ return retval;
+}
+
+static int ma901_set_stereo(struct ma901radio_device *radio, u8 stereo)
+{
+ int retval;
+
+ radio->buffer[0] = 0x0a;
+ radio->buffer[1] = MA901_RADIO_SET_MONO_STEREO;
+ radio->buffer[2] = stereo;
+ radio->buffer[3] = 0x00;
+ radio->buffer[4] = 0x00;
+ radio->buffer[5] = 0x00;
+ radio->buffer[6] = 0x00;
+ radio->buffer[7] = 0x00;
+
+ retval = usb_control_msg(radio->usbdev, usb_sndctrlpipe(radio->usbdev, 0),
+ 9, 0x21, 0x0300, 0,
+ radio->buffer, BUFFER_LENGTH, USB_TIMEOUT);
+
+ if (retval < 0)
+ return retval;
+
+ if (stereo == MA901_WANT_STEREO)
+ radio->stereo = V4L2_TUNER_MODE_STEREO;
+ else
+ radio->stereo = V4L2_TUNER_MODE_MONO;
+
+ return retval;
+}
+
+/* Handle unplugging the device.
+ * We call video_unregister_device in any case.
+ * The last function called in this procedure is
+ * usb_ma901radio_device_release.
+ */
+static void usb_ma901radio_disconnect(struct usb_interface *intf)
+{
+ struct ma901radio_device *radio = to_ma901radio_dev(usb_get_intfdata(intf));
+
+ mutex_lock(&radio->lock);
+ video_unregister_device(&radio->vdev);
+ usb_set_intfdata(intf, NULL);
+ v4l2_device_disconnect(&radio->v4l2_dev);
+ mutex_unlock(&radio->lock);
+ v4l2_device_put(&radio->v4l2_dev);
+}
+
+/* vidioc_querycap - query device capabilities */
+static int vidioc_querycap(struct file *file, void *priv,
+ struct v4l2_capability *v)
+{
+ struct ma901radio_device *radio = video_drvdata(file);
+
+ strscpy(v->driver, "radio-ma901", sizeof(v->driver));
+ strscpy(v->card, "Masterkit MA901 USB FM Radio", sizeof(v->card));
+ usb_make_path(radio->usbdev, v->bus_info, sizeof(v->bus_info));
+ return 0;
+}
+
+/* vidioc_g_tuner - get tuner attributes */
+static int vidioc_g_tuner(struct file *file, void *priv,
+ struct v4l2_tuner *v)
+{
+ struct ma901radio_device *radio = video_drvdata(file);
+
+ if (v->index > 0)
+ return -EINVAL;
+
+ v->signal = 0;
+
+ /* TODO: the same words like in _probe() goes here.
+ * When receiving of stats will be implemented then we can call
+ * ma901radio_get_stat().
+ * retval = ma901radio_get_stat(radio, &is_stereo, &v->signal);
+ */
+
+ strscpy(v->name, "FM", sizeof(v->name));
+ v->type = V4L2_TUNER_RADIO;
+ v->rangelow = FREQ_MIN * FREQ_MUL;
+ v->rangehigh = FREQ_MAX * FREQ_MUL;
+ v->capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO;
+ /* v->rxsubchans = is_stereo ? V4L2_TUNER_SUB_STEREO : V4L2_TUNER_SUB_MONO; */
+ v->audmode = radio->stereo ?
+ V4L2_TUNER_MODE_STEREO : V4L2_TUNER_MODE_MONO;
+ return 0;
+}
+
+/* vidioc_s_tuner - set tuner attributes */
+static int vidioc_s_tuner(struct file *file, void *priv,
+ const struct v4l2_tuner *v)
+{
+ struct ma901radio_device *radio = video_drvdata(file);
+
+ if (v->index > 0)
+ return -EINVAL;
+
+ /* mono/stereo selector */
+ switch (v->audmode) {
+ case V4L2_TUNER_MODE_MONO:
+ return ma901_set_stereo(radio, MA901_WANT_MONO);
+ default:
+ return ma901_set_stereo(radio, MA901_WANT_STEREO);
+ }
+}
+
+/* vidioc_s_frequency - set tuner radio frequency */
+static int vidioc_s_frequency(struct file *file, void *priv,
+ const struct v4l2_frequency *f)
+{
+ struct ma901radio_device *radio = video_drvdata(file);
+
+ if (f->tuner != 0)
+ return -EINVAL;
+
+ return ma901radio_set_freq(radio, clamp_t(unsigned, f->frequency,
+ FREQ_MIN * FREQ_MUL, FREQ_MAX * FREQ_MUL));
+}
+
+/* vidioc_g_frequency - get tuner radio frequency */
+static int vidioc_g_frequency(struct file *file, void *priv,
+ struct v4l2_frequency *f)
+{
+ struct ma901radio_device *radio = video_drvdata(file);
+
+ if (f->tuner != 0)
+ return -EINVAL;
+ f->frequency = radio->curfreq;
+
+ return 0;
+}
+
+static int usb_ma901radio_s_ctrl(struct v4l2_ctrl *ctrl)
+{
+ struct ma901radio_device *radio =
+ container_of(ctrl->handler, struct ma901radio_device, hdl);
+
+ switch (ctrl->id) {
+ case V4L2_CID_AUDIO_VOLUME: /* set volume */
+ return ma901radio_set_volume(radio, (u16)ctrl->val);
+ }
+
+ return -EINVAL;
+}
+
+/* TODO: Should we really need to implement suspend and resume functions?
+ * Radio has it's own memory and will continue playing if power is present
+ * on usb port and on resume it will start to play again based on freq, volume
+ * values in device memory.
+ */
+static int usb_ma901radio_suspend(struct usb_interface *intf, pm_message_t message)
+{
+ return 0;
+}
+
+static int usb_ma901radio_resume(struct usb_interface *intf)
+{
+ return 0;
+}
+
+static const struct v4l2_ctrl_ops usb_ma901radio_ctrl_ops = {
+ .s_ctrl = usb_ma901radio_s_ctrl,
+};
+
+/* File system interface */
+static const struct v4l2_file_operations usb_ma901radio_fops = {
+ .owner = THIS_MODULE,
+ .open = v4l2_fh_open,
+ .release = v4l2_fh_release,
+ .poll = v4l2_ctrl_poll,
+ .unlocked_ioctl = video_ioctl2,
+};
+
+static const struct v4l2_ioctl_ops usb_ma901radio_ioctl_ops = {
+ .vidioc_querycap = vidioc_querycap,
+ .vidioc_g_tuner = vidioc_g_tuner,
+ .vidioc_s_tuner = vidioc_s_tuner,
+ .vidioc_g_frequency = vidioc_g_frequency,
+ .vidioc_s_frequency = vidioc_s_frequency,
+ .vidioc_log_status = v4l2_ctrl_log_status,
+ .vidioc_subscribe_event = v4l2_ctrl_subscribe_event,
+ .vidioc_unsubscribe_event = v4l2_event_unsubscribe,
+};
+
+static void usb_ma901radio_release(struct v4l2_device *v4l2_dev)
+{
+ struct ma901radio_device *radio = to_ma901radio_dev(v4l2_dev);
+
+ v4l2_ctrl_handler_free(&radio->hdl);
+ v4l2_device_unregister(&radio->v4l2_dev);
+ kfree(radio->buffer);
+ kfree(radio);
+}
+
+/* check if the device is present and register with v4l and usb if it is */
+static int usb_ma901radio_probe(struct usb_interface *intf,
+ const struct usb_device_id *id)
+{
+ struct usb_device *dev = interface_to_usbdev(intf);
+ struct ma901radio_device *radio;
+ int retval = 0;
+
+ /* Masterkit MA901 usb radio has the same USB ID as many others
+ * Atmel V-USB devices. Let's make additional checks to be sure
+ * that this is our device.
+ */
+
+ if (dev->product && dev->manufacturer &&
+ (strncmp(dev->product, "MA901", 5) != 0
+ || strncmp(dev->manufacturer, "www.masterkit.ru", 16) != 0))
+ return -ENODEV;
+
+ radio = kzalloc(sizeof(struct ma901radio_device), GFP_KERNEL);
+ if (!radio) {
+ dev_err(&intf->dev, "kzalloc for ma901radio_device failed\n");
+ retval = -ENOMEM;
+ goto err;
+ }
+
+ radio->buffer = kmalloc(BUFFER_LENGTH, GFP_KERNEL);
+ if (!radio->buffer) {
+ dev_err(&intf->dev, "kmalloc for radio->buffer failed\n");
+ retval = -ENOMEM;
+ goto err_nobuf;
+ }
+
+ retval = v4l2_device_register(&intf->dev, &radio->v4l2_dev);
+ if (retval < 0) {
+ dev_err(&intf->dev, "couldn't register v4l2_device\n");
+ goto err_v4l2;
+ }
+
+ v4l2_ctrl_handler_init(&radio->hdl, 1);
+
+ /* TODO:It looks like this radio doesn't have mute/unmute control
+ * and windows program just emulate it using volume control.
+ * Let's plan to do the same in this driver.
+ *
+ * v4l2_ctrl_new_std(&radio->hdl, &usb_ma901radio_ctrl_ops,
+ * V4L2_CID_AUDIO_MUTE, 0, 1, 1, 1);
+ */
+
+ v4l2_ctrl_new_std(&radio->hdl, &usb_ma901radio_ctrl_ops,
+ V4L2_CID_AUDIO_VOLUME, MA901_VOLUME_MIN,
+ MA901_VOLUME_MAX, 1, MA901_VOLUME_MAX);
+
+ if (radio->hdl.error) {
+ retval = radio->hdl.error;
+ dev_err(&intf->dev, "couldn't register control\n");
+ goto err_ctrl;
+ }
+ mutex_init(&radio->lock);
+
+ radio->v4l2_dev.ctrl_handler = &radio->hdl;
+ radio->v4l2_dev.release = usb_ma901radio_release;
+ strscpy(radio->vdev.name, radio->v4l2_dev.name,
+ sizeof(radio->vdev.name));
+ radio->vdev.v4l2_dev = &radio->v4l2_dev;
+ radio->vdev.fops = &usb_ma901radio_fops;
+ radio->vdev.ioctl_ops = &usb_ma901radio_ioctl_ops;
+ radio->vdev.release = video_device_release_empty;
+ radio->vdev.lock = &radio->lock;
+ radio->vdev.device_caps = V4L2_CAP_RADIO | V4L2_CAP_TUNER;
+
+ radio->usbdev = interface_to_usbdev(intf);
+ radio->intf = intf;
+ usb_set_intfdata(intf, &radio->v4l2_dev);
+ radio->curfreq = 95.21 * FREQ_MUL;
+
+ video_set_drvdata(&radio->vdev, radio);
+
+ /* TODO: we can get some statistics (freq, volume) from device
+ * but it's not implemented yet. After insertion in usb-port radio
+ * setups frequency and starts playing without any initialization.
+ * So we don't call usb_ma901radio_init/get_stat() here.
+ * retval = usb_ma901radio_init(radio);
+ */
+
+ retval = video_register_device(&radio->vdev, VFL_TYPE_RADIO,
+ radio_nr);
+ if (retval < 0) {
+ dev_err(&intf->dev, "could not register video device\n");
+ goto err_vdev;
+ }
+
+ return 0;
+
+err_vdev:
+ v4l2_ctrl_handler_free(&radio->hdl);
+err_ctrl:
+ v4l2_device_unregister(&radio->v4l2_dev);
+err_v4l2:
+ kfree(radio->buffer);
+err_nobuf:
+ kfree(radio);
+err:
+ return retval;
+}
+
+/* USB Device ID List */
+static const struct usb_device_id usb_ma901radio_device_table[] = {
+ { USB_DEVICE_AND_INTERFACE_INFO(USB_MA901_VENDOR, USB_MA901_PRODUCT,
+ USB_CLASS_HID, 0, 0) },
+ { } /* Terminating entry */
+};
+
+MODULE_DEVICE_TABLE(usb, usb_ma901radio_device_table);
+
+/* USB subsystem interface */
+static struct usb_driver usb_ma901radio_driver = {
+ .name = MA901_DRIVER_NAME,
+ .probe = usb_ma901radio_probe,
+ .disconnect = usb_ma901radio_disconnect,
+ .suspend = usb_ma901radio_suspend,
+ .resume = usb_ma901radio_resume,
+ .reset_resume = usb_ma901radio_resume,
+ .id_table = usb_ma901radio_device_table,
+};
+
+module_usb_driver(usb_ma901radio_driver);