summaryrefslogtreecommitdiffstats
path: root/drivers/media/radio
diff options
context:
space:
mode:
authorSang-Heon Jeon <ekffu200098@gmail.com>2026-08-24 01:18:14 +0900
committerJulia Lawall <Julia.Lawall@inria.fr>2026-08-23 22:12:11 +0200
commit2bd30b8dc75b823dd7bbef9d7bc68e1fd7ecee5c (patch)
treedcc4da1d99f17541c5590ac63c77da4bf372a4ab /drivers/media/radio
downloadlinux-stable-2bd30b8dc75b823dd7bbef9d7bc68e1fd7ecee5c.tar.gz
linux-stable-2bd30b8dc75b823dd7bbef9d7bc68e1fd7ecee5c.zip
coccinelle: alloc_cast: drop removed allocatorsgrafted
- pci_alloc_consistent() and pci_zalloc_consistent() were removed by commit 7968778914e5 ("PCI: Remove the deprecated "pci-dma-compat.h" API") - kmem_alloc() was removed by commit f078d4ea8276 ("xfs: convert kmem_alloc() to kmalloc()") - kmem_zalloc() was removed by commit 10634530f7ba ("xfs: convert kmem_zalloc() to kzalloc()") - kmem_zone_alloc() and kmem_zone_zalloc() were removed by commit bae633a4a283 ("xfs: remove xfs_zone_{alloc,zalloc} helpers") So drop them from the rules. No functional change. Signed-off-by: Sang-Heon Jeon <ekffu200098@gmail.com> Signed-off-by: Julia Lawall <Julia.Lawall@inria.fr>
Diffstat (limited to 'drivers/media/radio')
-rw-r--r--drivers/media/radio/Kconfig488
-rw-r--r--drivers/media/radio/Makefile39
-rw-r--r--drivers/media/radio/dsbr100.c420
-rw-r--r--drivers/media/radio/lm7000.h44
-rw-r--r--drivers/media/radio/radio-aimslab.c195
-rw-r--r--drivers/media/radio/radio-aztech.c161
-rw-r--r--drivers/media/radio/radio-cadet.c679
-rw-r--r--drivers/media/radio/radio-gemtek.c340
-rw-r--r--drivers/media/radio/radio-isa.c377
-rw-r--r--drivers/media/radio/radio-isa.h101
-rw-r--r--drivers/media/radio/radio-keene.c405
-rw-r--r--drivers/media/radio/radio-ma901.c456
-rw-r--r--drivers/media/radio/radio-maxiradio.c203
-rw-r--r--drivers/media/radio/radio-miropcm20.c513
-rw-r--r--drivers/media/radio/radio-mr800.c599
-rw-r--r--drivers/media/radio/radio-raremono.c389
-rw-r--r--drivers/media/radio/radio-rtrack2.c142
-rw-r--r--drivers/media/radio/radio-sf16fmi.c382
-rw-r--r--drivers/media/radio/radio-sf16fmr2.c345
-rw-r--r--drivers/media/radio/radio-shark.c429
-rw-r--r--drivers/media/radio/radio-shark2.c395
-rw-r--r--drivers/media/radio/radio-si476x.c1523
-rw-r--r--drivers/media/radio/radio-tea5764.c529
-rw-r--r--drivers/media/radio/radio-tea5777.c587
-rw-r--r--drivers/media/radio/radio-tea5777.h76
-rw-r--r--drivers/media/radio/radio-terratec.c168
-rw-r--r--drivers/media/radio/radio-timb.c176
-rw-r--r--drivers/media/radio/radio-trust.c243
-rw-r--r--drivers/media/radio/radio-typhoon.c181
-rw-r--r--drivers/media/radio/radio-zoltrix.c249
-rw-r--r--drivers/media/radio/saa7706h.c417
-rw-r--r--drivers/media/radio/si470x/Kconfig52
-rw-r--r--drivers/media/radio/si470x/Makefile8
-rw-r--r--drivers/media/radio/si470x/radio-si470x-common.c794
-rw-r--r--drivers/media/radio/si470x/radio-si470x-i2c.c545
-rw-r--r--drivers/media/radio/si470x/radio-si470x-usb.c849
-rw-r--r--drivers/media/radio/si470x/radio-si470x.h220
-rw-r--r--drivers/media/radio/si4713/Kconfig41
-rw-r--r--drivers/media/radio/si4713/Makefile8
-rw-r--r--drivers/media/radio/si4713/radio-platform-si4713.c211
-rw-r--r--drivers/media/radio/si4713/radio-usb-si4713.c522
-rw-r--r--drivers/media/radio/si4713/si4713.c1665
-rw-r--r--drivers/media/radio/si4713/si4713.h254
-rw-r--r--drivers/media/radio/tea575x.c577
-rw-r--r--drivers/media/radio/tef6862.c195
45 files changed, 17192 insertions, 0 deletions
diff --git a/drivers/media/radio/Kconfig b/drivers/media/radio/Kconfig
new file mode 100644
index 000000000..bbbdd054b
--- /dev/null
+++ b/drivers/media/radio/Kconfig
@@ -0,0 +1,488 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Multimedia Video device configuration
+#
+
+menuconfig RADIO_ADAPTERS
+ tristate "Radio Adapters"
+ depends on VIDEO_DEV
+ depends on MEDIA_RADIO_SUPPORT
+ default VIDEO_DEV
+ help
+ Say Y here to enable selecting AM/FM radio adapters.
+
+if RADIO_ADAPTERS
+
+config RADIO_MAXIRADIO
+ tristate "Guillemot MAXI Radio FM 2000 radio"
+ depends on PCI && HAS_IOPORT
+ select RADIO_TEA575X
+ help
+ Choose Y here if you have this radio card. This card may also be
+ found as Gemtek PCI FM.
+
+ In order to control your radio card, you will need to use programs
+ that are compatible with the Video For Linux API. Information on
+ this API and pointers to "v4l" programs may be found at
+ <file:Documentation/userspace-api/media/index.rst>.
+
+ To compile this driver as a module, choose M here: the
+ module will be called radio-maxiradio.
+
+config RADIO_SAA7706H
+ tristate "SAA7706H Car Radio DSP"
+ depends on I2C
+ help
+ Say Y here if you want to use the SAA7706H Car radio Digital
+ Signal Processor, found for instance on the Russellville development
+ board. On the russellville the device is connected to internal
+ timberdale I2C bus.
+
+ To compile this driver as a module, choose M here: the
+ module will be called SAA7706H.
+
+config RADIO_SHARK
+ tristate "Griffin radioSHARK USB radio receiver"
+ depends on USB
+ select RADIO_TEA575X
+ help
+ Choose Y here if you have this radio receiver.
+
+ There are 2 versions of this device, this driver is for version 1,
+ which is white.
+
+ In order to control your radio card, you will need to use programs
+ that are compatible with the Video For Linux API. Information on
+ this API and pointers to "v4l" programs may be found at
+ <file:Documentation/userspace-api/media/index.rst>.
+
+ To compile this driver as a module, choose M here: the
+ module will be called radio-shark.
+
+config RADIO_SHARK2
+ tristate "Griffin radioSHARK2 USB radio receiver"
+ depends on USB
+ help
+ Choose Y here if you have this radio receiver.
+
+ There are 2 versions of this device, this driver is for version 2,
+ which is black.
+
+ In order to control your radio card, you will need to use programs
+ that are compatible with the Video For Linux API. Information on
+ this API and pointers to "v4l" programs may be found at
+ <file:Documentation/userspace-api/media/index.rst>.
+
+ To compile this driver as a module, choose M here: the
+ module will be called radio-shark2.
+
+config RADIO_SI4713
+ tristate "Silicon Labs Si4713 FM Radio with RDS Transmitter support"
+
+config RADIO_SI476X
+ tristate "Silicon Laboratories Si476x I2C FM Radio"
+ depends on I2C
+ depends on MFD_SI476X_CORE
+ depends on SND_SOC
+ select SND_SOC_SI476X
+ help
+ Choose Y here if you have this FM radio chip.
+
+ 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 "v4l2" programs may be found at
+ <file:Documentation/userspace-api/media/index.rst>.
+
+ To compile this driver as a module, choose M here: the
+ module will be called radio-si476x.
+
+config RADIO_TEA575X
+ tristate
+
+config RADIO_TEA5764
+ tristate "TEA5764 I2C FM radio support"
+ depends on I2C
+ help
+ Say Y here if you want to use the TEA5764 FM chip found in
+ EZX phones. This FM chip is present in EZX phones from Motorola,
+ connected to internal pxa I2C bus.
+
+ To compile this driver as a module, choose M here: the
+ module will be called radio-tea5764.
+
+config RADIO_TEA5764_XTAL
+ bool "TEA5764 crystal reference"
+ depends on RADIO_TEA5764=y
+ default y
+ help
+ Say Y here if TEA5764 have a 32768 Hz crystal in circuit, say N
+ here if TEA5764 reference frequency is connected in FREQIN.
+
+config RADIO_TEF6862
+ tristate "TEF6862 Car Radio Enhanced Selectivity Tuner"
+ depends on I2C
+ help
+ Say Y here if you want to use the TEF6862 Car Radio Enhanced
+ Selectivity Tuner, found for instance on the Russellville development
+ board. On the russellville the device is connected to internal
+ timberdale I2C bus.
+
+ To compile this driver as a module, choose M here: the
+ module will be called TEF6862.
+
+config RADIO_TIMBERDALE
+ tristate "Enable the Timberdale radio driver"
+ depends on MFD_TIMBERDALE
+ depends on I2C # for RADIO_SAA7706H
+ select RADIO_TEF6862
+ select RADIO_SAA7706H
+ help
+ This is a kind of umbrella driver for the Radio Tuner and DSP
+ found behind the Timberdale FPGA on the Russellville board.
+ Enabling this driver will automatically select the DSP and tuner.
+
+config USB_DSBR
+ tristate "D-Link/GemTek USB FM radio support"
+ depends on USB
+ 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 dsbr100.
+
+config USB_KEENE
+ tristate "Keene FM Transmitter USB support"
+ depends on USB
+ help
+ Say Y here if you want to connect this type of FM transmitter
+ to your computer's USB port.
+
+ To compile this driver as a module, choose M here: the
+ module will be called radio-keene.
+
+config USB_MA901
+ tristate "Masterkit MA901 USB FM radio support"
+ depends on USB
+ 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 or headphones.
+
+ To compile this driver as a module, choose M here: the
+ module will be called radio-ma901.
+
+config USB_MR800
+ tristate "AverMedia MR 800 USB FM radio support"
+ depends on USB
+ 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 radio-mr800.
+
+config USB_RAREMONO
+ tristate "Thanko's Raremono AM/FM/SW radio support"
+ depends on USB
+ help
+ The 'Thanko's Raremono' device contains the Si4734 chip from Silicon Labs Inc.
+ It is one of the very few or perhaps the only consumer USB radio device
+ to receive the AM/FM/SW bands.
+
+ Say Y here if you want to connect this type of AM/FM/SW receiver
+ to your computer's USB port.
+
+ To compile this driver as a module, choose M here: the
+ module will be called radio-raremono.
+
+source "drivers/media/radio/si470x/Kconfig"
+source "drivers/media/radio/si4713/Kconfig"
+
+#
+# ISA drivers configuration
+#
+
+menuconfig V4L_RADIO_ISA_DRIVERS
+ bool "ISA radio devices"
+ depends on ISA || COMPILE_TEST
+ depends on HAS_IOPORT
+ help
+ Say Y here to enable support for these ISA drivers.
+
+if V4L_RADIO_ISA_DRIVERS
+
+config RADIO_AZTECH
+ tristate "Aztech/Packard Bell Radio"
+ depends on ISA || COMPILE_TEST
+ depends on HAS_IOPORT
+ select RADIO_ISA
+ help
+ Choose Y here if you have one of these FM radio cards, and then fill
+ in the port address below.
+
+ To compile this driver as a module, choose M here: the
+ module will be called radio-aztech.
+
+config RADIO_AZTECH_PORT
+ hex "Aztech/Packard Bell I/O port (0x350 or 0x358)"
+ depends on RADIO_AZTECH=y
+ default "350"
+ help
+ Enter either 0x350 or 0x358 here. The card default is 0x350, if you
+ haven't changed the setting of jumper JP3 on the card. Removing the
+ jumper sets the card to 0x358.
+
+config RADIO_CADET
+ tristate "ADS Cadet AM/FM Tuner"
+ depends on ISA || COMPILE_TEST
+ depends on HAS_IOPORT
+ help
+ Choose Y here if you have one of these AM/FM radio cards, and then
+ fill in the port address below.
+
+ To compile this driver as a module, choose M here: the
+ module will be called radio-cadet.
+
+config RADIO_GEMTEK
+ tristate "GemTek Radio card (or compatible) support"
+ depends on ISA || COMPILE_TEST
+ depends on HAS_IOPORT
+ select RADIO_ISA
+ help
+ Choose Y here if you have this FM radio card, and then fill in the
+ I/O port address and settings below. The following cards either have
+ GemTek Radio tuner or are rebranded GemTek Radio cards:
+
+ - Sound Vision 16 Gold with FM Radio
+ - Typhoon Radio card (some models)
+ - Hama Radio card
+
+ To compile this driver as a module, choose M here: the
+ module will be called radio-gemtek.
+
+config RADIO_GEMTEK_PORT
+ hex "Fixed I/O port (0x20c, 0x30c, 0x24c, 0x34c, 0x248 or 0x28c)"
+ depends on RADIO_GEMTEK=y
+ default "34c"
+ help
+ Enter either 0x20c, 0x30c, 0x24c, 0x34c, 0x248 or 0x28c here. The
+ card default is 0x34c, if you haven't changed the jumper setting
+ on the card.
+
+ On Sound Vision 16 Gold PnP with FM Radio (ESS1869+FM Gemtek), the I/O
+ port is 0x20c, 0x248 or 0x28c.
+
+ If automatic I/O port probing is enabled this port will be used only
+ in case of automatic probing failure, ie. as a fallback.
+
+config RADIO_GEMTEK_PROBE
+ bool "Automatic I/O port probing"
+ depends on RADIO_GEMTEK=y
+ default y
+ help
+ Say Y here to enable automatic probing for GemTek Radio card. The
+ following ports will be probed: 0x20c, 0x30c, 0x24c, 0x34c, 0x248 and
+ 0x28c.
+
+config RADIO_ISA
+ depends on ISA || COMPILE_TEST
+ depends on HAS_IOPORT
+ tristate
+
+config RADIO_MIROPCM20
+ tristate "miroSOUND PCM20 radio"
+ depends on ISA || COMPILE_TEST
+ depends on ISA_DMA_API && SND
+ select SND_ISA
+ select SND_MIRO
+ help
+ Choose Y here if you have this FM radio card. You also need to enable
+ the ALSA sound system. This choice automatically selects the ALSA
+ sound card driver "Miro miroSOUND PCM1pro/PCM12/PCM20radio" as this
+ is required for the radio-miropcm20.
+
+ To compile this driver as a module, choose M here: the
+ module will be called radio-miropcm20.
+
+config RADIO_RTRACK
+ tristate "AIMSlab RadioTrack (aka RadioReveal) support"
+ depends on ISA || COMPILE_TEST
+ depends on HAS_IOPORT
+ select RADIO_ISA
+ help
+ Choose Y here if you have one of these FM radio cards, and then fill
+ in the port address below.
+
+ Note that newer AIMSlab RadioTrack cards have a different chipset
+ and are not supported by this driver. For these cards, use the
+ RadioTrack II driver below.
+
+ If you have a GemTeks combined (PnP) sound- and radio card you must
+ use this driver as a module and setup the card with isapnptools.
+ You must also pass the module a suitable io parameter, 0x248 has
+ been reported to be used by these cards.
+
+ More information is contained in the file
+ <file:Documentation/driver-api/media/drivers/radiotrack.rst>.
+
+ To compile this driver as a module, choose M here: the
+ module will be called radio-aimslab.
+
+config RADIO_RTRACK2
+ tristate "AIMSlab RadioTrack II support"
+ depends on ISA || COMPILE_TEST
+ select RADIO_ISA
+ help
+ Choose Y here if you have this FM radio card, and then fill in the
+ port address below.
+
+ Note: this driver hasn't been tested since a long time due to lack
+ of hardware. If you have this hardware, then please contact the
+ linux-media mailinglist.
+
+ To compile this driver as a module, choose M here: the
+ module will be called radio-rtrack2.
+
+config RADIO_RTRACK2_PORT
+ hex "RadioTrack II i/o port (0x20c or 0x30c)"
+ depends on RADIO_RTRACK2=y
+ default "30c"
+ help
+ Enter either 0x30c or 0x20c here. The card default is 0x30c, if you
+ haven't changed the jumper setting on the card.
+
+config RADIO_RTRACK_PORT
+ hex "RadioTrack i/o port (0x20f or 0x30f)"
+ depends on RADIO_RTRACK=y
+ default "30f"
+ help
+ Enter either 0x30f or 0x20f here. The card default is 0x30f, if you
+ haven't changed the jumper setting on the card.
+
+config RADIO_SF16FMI
+ tristate "SF16-FMI/SF16-FMP/SF16-FMD Radio"
+ depends on ISA || COMPILE_TEST
+ depends on HAS_IOPORT
+ help
+ Choose Y here if you have one of these FM radio cards.
+
+ To compile this driver as a module, choose M here: the
+ module will be called radio-sf16fmi.
+
+config RADIO_SF16FMR2
+ tristate "SF16-FMR2/SF16-FMD2 Radio"
+ depends on ISA || COMPILE_TEST
+ depends on HAS_IOPORT
+ select RADIO_TEA575X
+ help
+ Choose Y here if you have one of these FM radio cards.
+
+ To compile this driver as a module, choose M here: the
+ module will be called radio-sf16fmr2.
+
+config RADIO_TERRATEC
+ tristate "TerraTec ActiveRadio ISA Standalone"
+ depends on ISA || COMPILE_TEST
+ depends on HAS_IOPORT
+ select RADIO_ISA
+ help
+ Choose Y here if you have this FM radio card.
+
+ Note: this driver hasn't been tested since a long time due to lack
+ of hardware. If you have this hardware, then please contact the
+ linux-media mailinglist.
+
+ To compile this driver as a module, choose M here: the
+ module will be called radio-terratec.
+
+config RADIO_TRUST
+ tristate "Trust FM radio card"
+ depends on ISA || COMPILE_TEST
+ depends on HAS_IOPORT
+ select RADIO_ISA
+ help
+ This is a driver for the Trust FM radio cards. Say Y if you have
+ such a card and want to use it under Linux.
+
+ Note: this driver hasn't been tested since a long time due to lack
+ of hardware. If you have this hardware, then please contact the
+ linux-media mailinglist.
+
+ To compile this driver as a module, choose M here: the
+ module will be called radio-trust.
+
+config RADIO_TRUST_PORT
+ hex "Trust i/o port (usually 0x350 or 0x358)"
+ depends on RADIO_TRUST=y
+ default "350"
+ help
+ Enter the I/O port of your Trust FM radio card. If unsure, try the
+ values "0x350" or "0x358".
+
+config RADIO_TYPHOON
+ tristate "Typhoon Radio (a.k.a. EcoRadio)"
+ depends on ISA || COMPILE_TEST
+ depends on HAS_IOPORT
+ select RADIO_ISA
+ help
+ Choose Y here if you have one of these FM radio cards, and then fill
+ in the port address and the frequency used for muting below.
+
+ Note: this driver hasn't been tested since a long time due to lack
+ of hardware. If you have this hardware, then please contact the
+ linux-media mailinglist.
+
+ To compile this driver as a module, choose M here: the
+ module will be called radio-typhoon.
+
+config RADIO_TYPHOON_MUTEFREQ
+ int "Typhoon frequency set when muting the device (kHz)"
+ depends on RADIO_TYPHOON=y
+ default "87500"
+ help
+ Enter the frequency used for muting the radio. The device is never
+ completely silent. If the volume is just turned down, you can still
+ hear silent voices and music. For that reason, the frequency of the
+ radio device is set to the frequency you can enter here whenever
+ the device is muted. There should be no local radio station at that
+ frequency.
+
+config RADIO_TYPHOON_PORT
+ hex "Typhoon I/O port (0x316 or 0x336)"
+ depends on RADIO_TYPHOON=y
+ default "316"
+ help
+ Enter the I/O port of your Typhoon or EcoRadio radio card.
+
+config RADIO_ZOLTRIX
+ tristate "Zoltrix Radio"
+ depends on ISA || COMPILE_TEST
+ depends on HAS_IOPORT
+ select RADIO_ISA
+ help
+ Choose Y here if you have one of these FM radio cards, and then fill
+ in the port address below.
+
+ Note: this driver hasn't been tested since a long time due to lack
+ of hardware. If you have this hardware, then please contact the
+ linux-media mailinglist.
+
+ To compile this driver as a module, choose M here: the
+ module will be called radio-zoltrix.
+
+config RADIO_ZOLTRIX_PORT
+ hex "ZOLTRIX I/O port (0x20c or 0x30c)"
+ depends on RADIO_ZOLTRIX=y
+ default "20c"
+ help
+ Enter the I/O port of your Zoltrix radio card.
+
+endif # V4L_RADIO_ISA_DRIVERS
+
+endif # RADIO_ADAPTERS
diff --git a/drivers/media/radio/Makefile b/drivers/media/radio/Makefile
new file mode 100644
index 000000000..f54693ebd
--- /dev/null
+++ b/drivers/media/radio/Makefile
@@ -0,0 +1,39 @@
+# SPDX-License-Identifier: GPL-2.0
+#
+# Makefile for the kernel character device drivers.
+#
+
+shark2-objs := radio-shark2.o radio-tea5777.o
+
+# Please keep it alphabetically sorted by Kconfig name
+# (e. g. LC_ALL=C sort Makefile)
+obj-$(CONFIG_RADIO_AZTECH) += radio-aztech.o
+obj-$(CONFIG_RADIO_CADET) += radio-cadet.o
+obj-$(CONFIG_RADIO_GEMTEK) += radio-gemtek.o
+obj-$(CONFIG_RADIO_ISA) += radio-isa.o
+obj-$(CONFIG_RADIO_MAXIRADIO) += radio-maxiradio.o
+obj-$(CONFIG_RADIO_MIROPCM20) += radio-miropcm20.o
+obj-$(CONFIG_RADIO_RTRACK2) += radio-rtrack2.o
+obj-$(CONFIG_RADIO_RTRACK) += radio-aimslab.o
+obj-$(CONFIG_RADIO_SAA7706H) += saa7706h.o
+obj-$(CONFIG_RADIO_SF16FMI) += radio-sf16fmi.o
+obj-$(CONFIG_RADIO_SF16FMR2) += radio-sf16fmr2.o
+obj-$(CONFIG_RADIO_SHARK2) += shark2.o
+obj-$(CONFIG_RADIO_SHARK) += radio-shark.o
+obj-$(CONFIG_RADIO_SI470X) += si470x/
+obj-$(CONFIG_RADIO_SI4713) += si4713/
+obj-$(CONFIG_RADIO_SI476X) += radio-si476x.o
+obj-$(CONFIG_RADIO_TEA575X) += tea575x.o
+obj-$(CONFIG_RADIO_TEA5764) += radio-tea5764.o
+obj-$(CONFIG_RADIO_TEF6862) += tef6862.o
+obj-$(CONFIG_RADIO_TERRATEC) += radio-terratec.o
+obj-$(CONFIG_RADIO_TIMBERDALE) += radio-timb.o
+obj-$(CONFIG_RADIO_TRUST) += radio-trust.o
+obj-$(CONFIG_RADIO_TYPHOON) += radio-typhoon.o
+obj-$(CONFIG_RADIO_ZOLTRIX) += radio-zoltrix.o
+
+obj-$(CONFIG_USB_DSBR) += dsbr100.o
+obj-$(CONFIG_USB_KEENE) += radio-keene.o
+obj-$(CONFIG_USB_MA901) += radio-ma901.o
+obj-$(CONFIG_USB_MR800) += radio-mr800.o
+obj-$(CONFIG_USB_RAREMONO) += radio-raremono.o
diff --git a/drivers/media/radio/dsbr100.c b/drivers/media/radio/dsbr100.c
new file mode 100644
index 000000000..b8e0835a1
--- /dev/null
+++ b/drivers/media/radio/dsbr100.c
@@ -0,0 +1,420 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* A driver for the D-Link DSB-R100 USB radio and Gemtek USB Radio 21.
+ * The device plugs into both the USB and an analog audio input, so this thing
+ * only deals with initialisation and frequency setting, the
+ * audio data has to be handled by a sound driver.
+ *
+ * Major issue: I can't find out where the device reports the signal
+ * strength, and indeed the windows software appearantly just looks
+ * at the stereo indicator as well. So, scanning will only find
+ * stereo stations. Sad, but I can't help it.
+ *
+ * Also, the windows program sends oodles of messages over to the
+ * device, and I couldn't figure out their meaning. My suspicion
+ * is that they don't have any:-)
+ *
+ * You might find some interesting stuff about this module at
+ * http://unimut.fsk.uni-heidelberg.de/unimut/demi/dsbr
+ *
+ * Fully tested with the Keene USB FM Transmitter and the v4l2-compliance tool.
+ *
+ * Copyright (c) 2000 Markus Demleitner <msdemlei@cl.uni-heidelberg.de>
+*/
+
+#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 <linux/usb.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-ioctl.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-event.h>
+
+/*
+ * Version Information
+ */
+MODULE_AUTHOR("Markus Demleitner <msdemlei@tucana.harvard.edu>");
+MODULE_DESCRIPTION("D-Link DSB-R100 USB FM radio driver");
+MODULE_LICENSE("GPL");
+MODULE_VERSION("1.1.0");
+
+#define DSB100_VENDOR 0x04b4
+#define DSB100_PRODUCT 0x1002
+
+/* Commands the device appears to understand */
+#define DSB100_TUNE 1
+#define DSB100_ONOFF 2
+
+#define TB_LEN 16
+
+/* 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
+
+#define v4l2_dev_to_radio(d) container_of(d, struct dsbr100_device, v4l2_dev)
+
+static int radio_nr = -1;
+module_param(radio_nr, int, 0);
+
+/* Data for one (physical) device */
+struct dsbr100_device {
+ struct usb_device *usbdev;
+ struct video_device videodev;
+ struct v4l2_device v4l2_dev;
+ struct v4l2_ctrl_handler hdl;
+
+ u8 *transfer_buffer;
+ struct mutex v4l2_lock;
+ int curfreq;
+ bool stereo;
+ bool muted;
+};
+
+/* Low-level device interface begins here */
+
+/* set a frequency, freq is defined by v4l's TUNER_LOW, i.e. 1/16th kHz */
+static int dsbr100_setfreq(struct dsbr100_device *radio, unsigned freq)
+{
+ unsigned f = (freq / 16 * 80) / 1000 + 856;
+ int retval = 0;
+
+ if (!radio->muted) {
+ retval = usb_control_msg(radio->usbdev,
+ usb_rcvctrlpipe(radio->usbdev, 0),
+ DSB100_TUNE,
+ USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_DIR_IN,
+ (f >> 8) & 0x00ff, f & 0xff,
+ radio->transfer_buffer, 8, 300);
+ if (retval >= 0)
+ mdelay(1);
+ }
+
+ if (retval >= 0) {
+ radio->curfreq = freq;
+ return 0;
+ }
+ dev_err(&radio->usbdev->dev,
+ "%s - usb_control_msg returned %i, request %i\n",
+ __func__, retval, DSB100_TUNE);
+ return retval;
+}
+
+/* switch on radio */
+static int dsbr100_start(struct dsbr100_device *radio)
+{
+ int retval = usb_control_msg(radio->usbdev,
+ usb_rcvctrlpipe(radio->usbdev, 0),
+ DSB100_ONOFF,
+ USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_DIR_IN,
+ 0x01, 0x00, radio->transfer_buffer, 8, 300);
+
+ if (retval >= 0)
+ return dsbr100_setfreq(radio, radio->curfreq);
+ dev_err(&radio->usbdev->dev,
+ "%s - usb_control_msg returned %i, request %i\n",
+ __func__, retval, DSB100_ONOFF);
+ return retval;
+
+}
+
+/* switch off radio */
+static int dsbr100_stop(struct dsbr100_device *radio)
+{
+ int retval = usb_control_msg(radio->usbdev,
+ usb_rcvctrlpipe(radio->usbdev, 0),
+ DSB100_ONOFF,
+ USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_DIR_IN,
+ 0x00, 0x00, radio->transfer_buffer, 8, 300);
+
+ if (retval >= 0)
+ return 0;
+ dev_err(&radio->usbdev->dev,
+ "%s - usb_control_msg returned %i, request %i\n",
+ __func__, retval, DSB100_ONOFF);
+ return retval;
+
+}
+
+/* return the device status. This is, in effect, just whether it
+sees a stereo signal or not. Pity. */
+static void dsbr100_getstat(struct dsbr100_device *radio)
+{
+ int retval = usb_control_msg(radio->usbdev,
+ usb_rcvctrlpipe(radio->usbdev, 0),
+ USB_REQ_GET_STATUS,
+ USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_DIR_IN,
+ 0x00, 0x24, radio->transfer_buffer, 8, 300);
+
+ if (retval < 0) {
+ radio->stereo = false;
+ dev_err(&radio->usbdev->dev,
+ "%s - usb_control_msg returned %i, request %i\n",
+ __func__, retval, USB_REQ_GET_STATUS);
+ } else {
+ radio->stereo = !(radio->transfer_buffer[0] & 0x01);
+ }
+}
+
+static int vidioc_querycap(struct file *file, void *priv,
+ struct v4l2_capability *v)
+{
+ struct dsbr100_device *radio = video_drvdata(file);
+
+ strscpy(v->driver, "dsbr100", sizeof(v->driver));
+ strscpy(v->card, "D-Link R-100 USB FM Radio", sizeof(v->card));
+ usb_make_path(radio->usbdev, v->bus_info, sizeof(v->bus_info));
+ return 0;
+}
+
+static int vidioc_g_tuner(struct file *file, void *priv,
+ struct v4l2_tuner *v)
+{
+ struct dsbr100_device *radio = video_drvdata(file);
+
+ if (v->index > 0)
+ return -EINVAL;
+
+ dsbr100_getstat(radio);
+ 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->rxsubchans = radio->stereo ? V4L2_TUNER_SUB_STEREO :
+ V4L2_TUNER_SUB_MONO;
+ v->capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO;
+ v->audmode = V4L2_TUNER_MODE_STEREO;
+ v->signal = radio->stereo ? 0xffff : 0; /* We can't get the signal strength */
+ return 0;
+}
+
+static int vidioc_s_tuner(struct file *file, void *priv,
+ const struct v4l2_tuner *v)
+{
+ return v->index ? -EINVAL : 0;
+}
+
+static int vidioc_s_frequency(struct file *file, void *priv,
+ const struct v4l2_frequency *f)
+{
+ struct dsbr100_device *radio = video_drvdata(file);
+
+ if (f->tuner != 0 || f->type != V4L2_TUNER_RADIO)
+ return -EINVAL;
+
+ return dsbr100_setfreq(radio, clamp_t(unsigned, f->frequency,
+ FREQ_MIN * FREQ_MUL, FREQ_MAX * FREQ_MUL));
+}
+
+static int vidioc_g_frequency(struct file *file, void *priv,
+ struct v4l2_frequency *f)
+{
+ struct dsbr100_device *radio = video_drvdata(file);
+
+ if (f->tuner)
+ return -EINVAL;
+ f->type = V4L2_TUNER_RADIO;
+ f->frequency = radio->curfreq;
+ return 0;
+}
+
+static int usb_dsbr100_s_ctrl(struct v4l2_ctrl *ctrl)
+{
+ struct dsbr100_device *radio =
+ container_of(ctrl->handler, struct dsbr100_device, hdl);
+
+ switch (ctrl->id) {
+ case V4L2_CID_AUDIO_MUTE:
+ radio->muted = ctrl->val;
+ return radio->muted ? dsbr100_stop(radio) : dsbr100_start(radio);
+ }
+ return -EINVAL;
+}
+
+
+/* USB subsystem interface begins here */
+
+/*
+ * Handle unplugging of the device.
+ * We call video_unregister_device in any case.
+ * The last function called in this procedure is
+ * usb_dsbr100_video_device_release
+ */
+static void usb_dsbr100_disconnect(struct usb_interface *intf)
+{
+ struct dsbr100_device *radio = usb_get_intfdata(intf);
+
+ mutex_lock(&radio->v4l2_lock);
+ /*
+ * Disconnect is also called on unload, and in that case we need to
+ * mute the device. This call will silently fail if it is called
+ * after a physical disconnect.
+ */
+ usb_control_msg(radio->usbdev,
+ usb_rcvctrlpipe(radio->usbdev, 0),
+ DSB100_ONOFF,
+ USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_DIR_IN,
+ 0x00, 0x00, radio->transfer_buffer, 8, 300);
+ usb_set_intfdata(intf, NULL);
+ video_unregister_device(&radio->videodev);
+ v4l2_device_disconnect(&radio->v4l2_dev);
+ mutex_unlock(&radio->v4l2_lock);
+ v4l2_device_put(&radio->v4l2_dev);
+}
+
+
+/* Suspend device - stop device. */
+static int usb_dsbr100_suspend(struct usb_interface *intf, pm_message_t message)
+{
+ struct dsbr100_device *radio = usb_get_intfdata(intf);
+
+ mutex_lock(&radio->v4l2_lock);
+ if (!radio->muted && dsbr100_stop(radio) < 0)
+ dev_warn(&intf->dev, "dsbr100_stop failed\n");
+ mutex_unlock(&radio->v4l2_lock);
+
+ dev_info(&intf->dev, "going into suspend..\n");
+ return 0;
+}
+
+/* Resume device - start device. */
+static int usb_dsbr100_resume(struct usb_interface *intf)
+{
+ struct dsbr100_device *radio = usb_get_intfdata(intf);
+
+ mutex_lock(&radio->v4l2_lock);
+ if (!radio->muted && dsbr100_start(radio) < 0)
+ dev_warn(&intf->dev, "dsbr100_start failed\n");
+ mutex_unlock(&radio->v4l2_lock);
+
+ dev_info(&intf->dev, "coming out of suspend..\n");
+ return 0;
+}
+
+/* free data structures */
+static void usb_dsbr100_release(struct v4l2_device *v4l2_dev)
+{
+ struct dsbr100_device *radio = v4l2_dev_to_radio(v4l2_dev);
+
+ v4l2_ctrl_handler_free(&radio->hdl);
+ v4l2_device_unregister(&radio->v4l2_dev);
+ kfree(radio->transfer_buffer);
+ kfree(radio);
+}
+
+static const struct v4l2_ctrl_ops usb_dsbr100_ctrl_ops = {
+ .s_ctrl = usb_dsbr100_s_ctrl,
+};
+
+/* File system interface */
+static const struct v4l2_file_operations usb_dsbr100_fops = {
+ .owner = THIS_MODULE,
+ .unlocked_ioctl = video_ioctl2,
+ .open = v4l2_fh_open,
+ .release = v4l2_fh_release,
+ .poll = v4l2_ctrl_poll,
+};
+
+static const struct v4l2_ioctl_ops usb_dsbr100_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,
+};
+
+/* check if the device is present and register with v4l and usb if it is */
+static int usb_dsbr100_probe(struct usb_interface *intf,
+ const struct usb_device_id *id)
+{
+ struct dsbr100_device *radio;
+ struct v4l2_device *v4l2_dev;
+ int retval;
+
+ radio = kzalloc_obj(struct dsbr100_device);
+
+ if (!radio)
+ return -ENOMEM;
+
+ radio->transfer_buffer = kmalloc(TB_LEN, GFP_KERNEL);
+
+ if (!(radio->transfer_buffer)) {
+ kfree(radio);
+ return -ENOMEM;
+ }
+
+ v4l2_dev = &radio->v4l2_dev;
+ v4l2_dev->release = usb_dsbr100_release;
+
+ retval = v4l2_device_register(&intf->dev, v4l2_dev);
+ if (retval < 0) {
+ v4l2_err(v4l2_dev, "couldn't register v4l2_device\n");
+ goto err_reg_dev;
+ }
+
+ v4l2_ctrl_handler_init(&radio->hdl, 1);
+ v4l2_ctrl_new_std(&radio->hdl, &usb_dsbr100_ctrl_ops,
+ V4L2_CID_AUDIO_MUTE, 0, 1, 1, 1);
+ if (radio->hdl.error) {
+ retval = radio->hdl.error;
+ v4l2_err(v4l2_dev, "couldn't register control\n");
+ goto err_reg_ctrl;
+ }
+ mutex_init(&radio->v4l2_lock);
+ strscpy(radio->videodev.name, v4l2_dev->name,
+ sizeof(radio->videodev.name));
+ radio->videodev.v4l2_dev = v4l2_dev;
+ radio->videodev.fops = &usb_dsbr100_fops;
+ radio->videodev.ioctl_ops = &usb_dsbr100_ioctl_ops;
+ radio->videodev.release = video_device_release_empty;
+ radio->videodev.lock = &radio->v4l2_lock;
+ radio->videodev.ctrl_handler = &radio->hdl;
+ radio->videodev.device_caps = V4L2_CAP_RADIO | V4L2_CAP_TUNER;
+
+ radio->usbdev = interface_to_usbdev(intf);
+ radio->curfreq = FREQ_MIN * FREQ_MUL;
+ radio->muted = true;
+
+ video_set_drvdata(&radio->videodev, radio);
+ usb_set_intfdata(intf, radio);
+
+ retval = video_register_device(&radio->videodev, VFL_TYPE_RADIO, radio_nr);
+ if (retval == 0)
+ return 0;
+ v4l2_err(v4l2_dev, "couldn't register video device\n");
+
+err_reg_ctrl:
+ v4l2_ctrl_handler_free(&radio->hdl);
+ v4l2_device_unregister(v4l2_dev);
+err_reg_dev:
+ kfree(radio->transfer_buffer);
+ kfree(radio);
+ return retval;
+}
+
+static const struct usb_device_id usb_dsbr100_device_table[] = {
+ { USB_DEVICE(DSB100_VENDOR, DSB100_PRODUCT) },
+ { } /* Terminating entry */
+};
+
+MODULE_DEVICE_TABLE(usb, usb_dsbr100_device_table);
+
+/* USB subsystem interface */
+static struct usb_driver usb_dsbr100_driver = {
+ .name = "dsbr100",
+ .probe = usb_dsbr100_probe,
+ .disconnect = usb_dsbr100_disconnect,
+ .id_table = usb_dsbr100_device_table,
+ .suspend = usb_dsbr100_suspend,
+ .resume = usb_dsbr100_resume,
+ .reset_resume = usb_dsbr100_resume,
+};
+
+module_usb_driver(usb_dsbr100_driver);
diff --git a/drivers/media/radio/lm7000.h b/drivers/media/radio/lm7000.h
new file mode 100644
index 000000000..adb217965
--- /dev/null
+++ b/drivers/media/radio/lm7000.h
@@ -0,0 +1,44 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef __LM7000_H
+#define __LM7000_H
+
+/* Sanyo LM7000 tuner chip control
+ *
+ * Copyright 2012 Ondrej Zary <linux@rainbow-software.org>
+ * based on radio-aimslab.c by M. Kirkwood
+ * and radio-sf16fmi.c by M. Kirkwood and Petr Vandrovec
+ */
+
+#define LM7000_DATA (1 << 0)
+#define LM7000_CLK (1 << 1)
+#define LM7000_CE (1 << 2)
+
+#define LM7000_FM_100 (0 << 20)
+#define LM7000_FM_50 (1 << 20)
+#define LM7000_FM_25 (2 << 20)
+#define LM7000_BIT_FM (1 << 23)
+
+static inline void lm7000_set_freq(u32 freq, void *handle,
+ void (*set_pins)(void *handle, u8 pins))
+{
+ int i;
+ u8 data;
+ u32 val;
+
+ freq += 171200; /* Add 10.7 MHz IF */
+ freq /= 400; /* Convert to 25 kHz units */
+ val = freq | LM7000_FM_25 | LM7000_BIT_FM;
+ /* write the 24-bit register, starting with LSB */
+ for (i = 0; i < 24; i++) {
+ data = val & (1 << i) ? LM7000_DATA : 0;
+ set_pins(handle, data | LM7000_CE);
+ udelay(2);
+ set_pins(handle, data | LM7000_CE | LM7000_CLK);
+ udelay(2);
+ set_pins(handle, data | LM7000_CE);
+ udelay(2);
+ }
+ set_pins(handle, 0);
+}
+
+#endif /* __LM7000_H */
diff --git a/drivers/media/radio/radio-aimslab.c b/drivers/media/radio/radio-aimslab.c
new file mode 100644
index 000000000..1395f76b0
--- /dev/null
+++ b/drivers/media/radio/radio-aimslab.c
@@ -0,0 +1,195 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * AimsLab RadioTrack (aka RadioVeveal) driver
+ *
+ * Copyright 1997 M. Kirkwood
+ *
+ * Converted to the radio-isa framework by Hans Verkuil <hverkuil@kernel.org>
+ * Converted to V4L2 API by Mauro Carvalho Chehab <mchehab@kernel.org>
+ * Converted to new API by Alan Cox <alan@lxorguk.ukuu.org.uk>
+ * Various bugfixes and enhancements by Russell Kroll <rkroll@exploits.org>
+ *
+ * Notes on the hardware (reverse engineered from other peoples'
+ * reverse engineering of AIMS' code :-)
+ *
+ * Frequency control is done digitally -- ie out(port,encodefreq(95.8));
+ *
+ * The signal strength query is unsurprisingly inaccurate. And it seems
+ * to indicate that (on my card, at least) the frequency setting isn't
+ * too great. (I have to tune up .025MHz from what the freq should be
+ * to get a report that the thing is tuned.)
+ *
+ * Volume control is (ugh) analogue:
+ * out(port, start_increasing_volume);
+ * wait(a_wee_while);
+ * out(port, stop_changing_the_volume);
+ *
+ * Fully tested with the Keene USB FM Transmitter and the v4l2-compliance tool.
+ */
+
+#include <linux/module.h> /* Modules */
+#include <linux/init.h> /* Initdata */
+#include <linux/ioport.h> /* request_region */
+#include <linux/delay.h> /* msleep */
+#include <linux/videodev2.h> /* kernel radio structs */
+#include <linux/io.h> /* outb, outb_p */
+#include <linux/slab.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-ioctl.h>
+#include <media/v4l2-ctrls.h>
+#include "radio-isa.h"
+#include "lm7000.h"
+
+MODULE_AUTHOR("M. Kirkwood");
+MODULE_DESCRIPTION("A driver for the RadioTrack/RadioReveal radio card.");
+MODULE_LICENSE("GPL");
+MODULE_VERSION("1.0.0");
+
+#ifndef CONFIG_RADIO_RTRACK_PORT
+#define CONFIG_RADIO_RTRACK_PORT -1
+#endif
+
+#define RTRACK_MAX 2
+
+static int io[RTRACK_MAX] = { [0] = CONFIG_RADIO_RTRACK_PORT,
+ [1 ... (RTRACK_MAX - 1)] = -1 };
+static int radio_nr[RTRACK_MAX] = { [0 ... (RTRACK_MAX - 1)] = -1 };
+
+module_param_array(io, int, NULL, 0444);
+MODULE_PARM_DESC(io, "I/O addresses of the RadioTrack card (0x20f or 0x30f)");
+module_param_array(radio_nr, int, NULL, 0444);
+MODULE_PARM_DESC(radio_nr, "Radio device numbers");
+
+struct rtrack {
+ struct radio_isa_card isa;
+ int curvol;
+};
+
+static struct radio_isa_card *rtrack_alloc(void)
+{
+ struct rtrack *rt = kzalloc_obj(struct rtrack);
+
+ if (rt)
+ rt->curvol = 0xff;
+ return rt ? &rt->isa : NULL;
+}
+
+#define AIMS_BIT_TUN_CE (1 << 0)
+#define AIMS_BIT_TUN_CLK (1 << 1)
+#define AIMS_BIT_TUN_DATA (1 << 2)
+#define AIMS_BIT_VOL_CE (1 << 3)
+#define AIMS_BIT_TUN_STRQ (1 << 4)
+/* bit 5 is not connected */
+#define AIMS_BIT_VOL_UP (1 << 6) /* active low */
+#define AIMS_BIT_VOL_DN (1 << 7) /* active low */
+
+static void rtrack_set_pins(void *handle, u8 pins)
+{
+ struct radio_isa_card *isa = handle;
+ struct rtrack *rt = container_of(isa, struct rtrack, isa);
+ u8 bits = AIMS_BIT_VOL_DN | AIMS_BIT_VOL_UP | AIMS_BIT_TUN_STRQ;
+
+ if (!v4l2_ctrl_g_ctrl(rt->isa.mute))
+ bits |= AIMS_BIT_VOL_CE;
+
+ if (pins & LM7000_DATA)
+ bits |= AIMS_BIT_TUN_DATA;
+ if (pins & LM7000_CLK)
+ bits |= AIMS_BIT_TUN_CLK;
+ if (pins & LM7000_CE)
+ bits |= AIMS_BIT_TUN_CE;
+
+ outb_p(bits, rt->isa.io);
+}
+
+static int rtrack_s_frequency(struct radio_isa_card *isa, u32 freq)
+{
+ lm7000_set_freq(freq, isa, rtrack_set_pins);
+
+ return 0;
+}
+
+static u32 rtrack_g_signal(struct radio_isa_card *isa)
+{
+ /* bit set = no signal present */
+ return 0xffff * !(inb(isa->io) & 2);
+}
+
+static int rtrack_s_mute_volume(struct radio_isa_card *isa, bool mute, int vol)
+{
+ struct rtrack *rt = container_of(isa, struct rtrack, isa);
+ int curvol = rt->curvol;
+
+ if (mute) {
+ outb(0xd0, isa->io); /* volume steady + sigstr + off */
+ return 0;
+ }
+ if (vol == 0) { /* volume = 0 means mute the card */
+ outb(0x48, isa->io); /* volume down but still "on" */
+ msleep(curvol * 3); /* make sure it's totally down */
+ } else if (curvol < vol) {
+ outb(0x98, isa->io); /* volume up + sigstr + on */
+ for (; curvol < vol; curvol++)
+ mdelay(3);
+ } else if (curvol > vol) {
+ outb(0x58, isa->io); /* volume down + sigstr + on */
+ for (; curvol > vol; curvol--)
+ mdelay(3);
+ }
+ outb(0xd8, isa->io); /* volume steady + sigstr + on */
+ rt->curvol = vol;
+ return 0;
+}
+
+/* Mute card - prevents noisy bootups */
+static int rtrack_initialize(struct radio_isa_card *isa)
+{
+ /* this ensures that the volume is all the way up */
+ outb(0x90, isa->io); /* volume up but still "on" */
+ msleep(3000); /* make sure it's totally up */
+ outb(0xc0, isa->io); /* steady volume, mute card */
+ return 0;
+}
+
+static const struct radio_isa_ops rtrack_ops = {
+ .alloc = rtrack_alloc,
+ .init = rtrack_initialize,
+ .s_mute_volume = rtrack_s_mute_volume,
+ .s_frequency = rtrack_s_frequency,
+ .g_signal = rtrack_g_signal,
+};
+
+static const int rtrack_ioports[] = { 0x20f, 0x30f };
+
+static struct radio_isa_driver rtrack_driver = {
+ .driver = {
+ .match = radio_isa_match,
+ .probe = radio_isa_probe,
+ .remove = radio_isa_remove,
+ .driver = {
+ .name = "radio-aimslab",
+ },
+ },
+ .io_params = io,
+ .radio_nr_params = radio_nr,
+ .io_ports = rtrack_ioports,
+ .num_of_io_ports = ARRAY_SIZE(rtrack_ioports),
+ .region_size = 2,
+ .card = "AIMSlab RadioTrack/RadioReveal",
+ .ops = &rtrack_ops,
+ .has_stereo = true,
+ .max_volume = 0xff,
+};
+
+static int __init rtrack_init(void)
+{
+ return isa_register_driver(&rtrack_driver.driver, RTRACK_MAX);
+}
+
+static void __exit rtrack_exit(void)
+{
+ isa_unregister_driver(&rtrack_driver.driver);
+}
+
+module_init(rtrack_init);
+module_exit(rtrack_exit);
diff --git a/drivers/media/radio/radio-aztech.c b/drivers/media/radio/radio-aztech.c
new file mode 100644
index 000000000..175ef2b0c
--- /dev/null
+++ b/drivers/media/radio/radio-aztech.c
@@ -0,0 +1,161 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * radio-aztech.c - Aztech radio card driver
+ *
+ * Converted to the radio-isa framework by Hans Verkuil <hverkuil@kernel.org>
+ * Converted to V4L2 API by Mauro Carvalho Chehab <mchehab@kernel.org>
+ * Adapted to support the Video for Linux API by
+ * Russell Kroll <rkroll@exploits.org>. Based on original tuner code by:
+ *
+ * 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)
+ *
+ * Fully tested with the Keene USB FM Transmitter and the v4l2-compliance tool.
+*/
+
+#include <linux/module.h> /* Modules */
+#include <linux/init.h> /* Initdata */
+#include <linux/ioport.h> /* request_region */
+#include <linux/delay.h> /* udelay */
+#include <linux/videodev2.h> /* kernel radio structs */
+#include <linux/io.h> /* outb, outb_p */
+#include <linux/slab.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-ioctl.h>
+#include <media/v4l2-ctrls.h>
+#include "radio-isa.h"
+#include "lm7000.h"
+
+MODULE_AUTHOR("Russell Kroll, Quay Lu, Donald Song, Jason Lewis, Scott McGrath, William McGrath");
+MODULE_DESCRIPTION("A driver for the Aztech radio card.");
+MODULE_LICENSE("GPL");
+MODULE_VERSION("1.0.0");
+
+/* acceptable ports: 0x350 (JP3 shorted), 0x358 (JP3 open) */
+#ifndef CONFIG_RADIO_AZTECH_PORT
+#define CONFIG_RADIO_AZTECH_PORT -1
+#endif
+
+#define AZTECH_MAX 2
+
+static int io[AZTECH_MAX] = { [0] = CONFIG_RADIO_AZTECH_PORT,
+ [1 ... (AZTECH_MAX - 1)] = -1 };
+static int radio_nr[AZTECH_MAX] = { [0 ... (AZTECH_MAX - 1)] = -1 };
+
+module_param_array(io, int, NULL, 0444);
+MODULE_PARM_DESC(io, "I/O addresses of the Aztech card (0x350 or 0x358)");
+module_param_array(radio_nr, int, NULL, 0444);
+MODULE_PARM_DESC(radio_nr, "Radio device numbers");
+
+struct aztech {
+ struct radio_isa_card isa;
+ int curvol;
+};
+
+/* bit definitions for register read */
+#define AZTECH_BIT_NOT_TUNED (1 << 0)
+#define AZTECH_BIT_MONO (1 << 1)
+/* bit definitions for register write */
+#define AZTECH_BIT_TUN_CE (1 << 1)
+#define AZTECH_BIT_TUN_CLK (1 << 6)
+#define AZTECH_BIT_TUN_DATA (1 << 7)
+/* bits 0 and 2 are volume control, bits 3..5 are not connected */
+
+static void aztech_set_pins(void *handle, u8 pins)
+{
+ struct radio_isa_card *isa = handle;
+ struct aztech *az = container_of(isa, struct aztech, isa);
+ u8 bits = az->curvol;
+
+ if (pins & LM7000_DATA)
+ bits |= AZTECH_BIT_TUN_DATA;
+ if (pins & LM7000_CLK)
+ bits |= AZTECH_BIT_TUN_CLK;
+ if (pins & LM7000_CE)
+ bits |= AZTECH_BIT_TUN_CE;
+
+ outb_p(bits, az->isa.io);
+}
+
+static struct radio_isa_card *aztech_alloc(void)
+{
+ struct aztech *az = kzalloc_obj(*az);
+
+ return az ? &az->isa : NULL;
+}
+
+static int aztech_s_frequency(struct radio_isa_card *isa, u32 freq)
+{
+ lm7000_set_freq(freq, isa, aztech_set_pins);
+
+ return 0;
+}
+
+static u32 aztech_g_rxsubchans(struct radio_isa_card *isa)
+{
+ if (inb(isa->io) & AZTECH_BIT_MONO)
+ return V4L2_TUNER_SUB_MONO;
+ return V4L2_TUNER_SUB_STEREO;
+}
+
+static u32 aztech_g_signal(struct radio_isa_card *isa)
+{
+ return (inb(isa->io) & AZTECH_BIT_NOT_TUNED) ? 0 : 0xffff;
+}
+
+static int aztech_s_mute_volume(struct radio_isa_card *isa, bool mute, int vol)
+{
+ struct aztech *az = container_of(isa, struct aztech, isa);
+
+ if (mute)
+ vol = 0;
+ az->curvol = (vol & 1) + ((vol & 2) << 1);
+ outb(az->curvol, isa->io);
+ return 0;
+}
+
+static const struct radio_isa_ops aztech_ops = {
+ .alloc = aztech_alloc,
+ .s_mute_volume = aztech_s_mute_volume,
+ .s_frequency = aztech_s_frequency,
+ .g_rxsubchans = aztech_g_rxsubchans,
+ .g_signal = aztech_g_signal,
+};
+
+static const int aztech_ioports[] = { 0x350, 0x358 };
+
+static struct radio_isa_driver aztech_driver = {
+ .driver = {
+ .match = radio_isa_match,
+ .probe = radio_isa_probe,
+ .remove = radio_isa_remove,
+ .driver = {
+ .name = "radio-aztech",
+ },
+ },
+ .io_params = io,
+ .radio_nr_params = radio_nr,
+ .io_ports = aztech_ioports,
+ .num_of_io_ports = ARRAY_SIZE(aztech_ioports),
+ .region_size = 8,
+ .card = "Aztech Radio",
+ .ops = &aztech_ops,
+ .has_stereo = true,
+ .max_volume = 3,
+};
+
+static int __init aztech_init(void)
+{
+ return isa_register_driver(&aztech_driver.driver, AZTECH_MAX);
+}
+
+static void __exit aztech_exit(void)
+{
+ isa_unregister_driver(&aztech_driver.driver);
+}
+
+module_init(aztech_init);
+module_exit(aztech_exit);
diff --git a/drivers/media/radio/radio-cadet.c b/drivers/media/radio/radio-cadet.c
new file mode 100644
index 000000000..53c1d1061
--- /dev/null
+++ b/drivers/media/radio/radio-cadet.c
@@ -0,0 +1,679 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* 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@lxorguk.ukuu.org.uk>
+ * Cleaned up locking, delay code, general odds and ends
+ *
+ * 2006-07-30 Hans J. Koch <koch@hjk-az.de>
+ * Changed API to V4L2
+ */
+
+#include <linux/module.h> /* Modules */
+#include <linux/init.h> /* Initdata */
+#include <linux/ioport.h> /* request_region */
+#include <linux/delay.h> /* udelay */
+#include <linux/videodev2.h> /* V4L2 API defs */
+#include <linux/param.h>
+#include <linux/pnp.h>
+#include <linux/sched.h>
+#include <linux/io.h> /* outb, outb_p */
+#include <media/v4l2-device.h>
+#include <media/v4l2-ioctl.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-fh.h>
+#include <media/v4l2-event.h>
+
+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_VERSION("0.3.4");
+
+static int io = -1; /* default to isapnp activation */
+static int radio_nr = -1;
+
+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);
+
+#define RDS_BUFFER 256
+#define RDS_RX_FLAG 1
+#define MBS_RX_FLAG 2
+
+struct cadet {
+ struct v4l2_device v4l2_dev;
+ struct video_device vdev;
+ struct v4l2_ctrl_handler ctrl_handler;
+ int io;
+ bool is_fm_band;
+ u32 curfreq;
+ int tunestat;
+ int sigstrength;
+ wait_queue_head_t read_queue;
+ struct timer_list readtimer;
+ u8 rdsin, rdsout, rdsstat;
+ unsigned char rdsbuf[RDS_BUFFER];
+ struct mutex lock;
+ int reading;
+};
+
+static struct cadet cadet_card;
+
+/*
+ * 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] = {
+ { 1835, 2621, 4128, 65535 },
+ { 2185, 4369, 13107, 65535 },
+};
+
+static const struct v4l2_frequency_band bands[] = {
+ {
+ .index = 0,
+ .type = V4L2_TUNER_RADIO,
+ .capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_FREQ_BANDS,
+ .rangelow = 8320, /* 520 kHz */
+ .rangehigh = 26400, /* 1650 kHz */
+ .modulation = V4L2_BAND_MODULATION_AM,
+ }, {
+ .index = 1,
+ .type = V4L2_TUNER_RADIO,
+ .capability = V4L2_TUNER_CAP_STEREO | V4L2_TUNER_CAP_RDS |
+ V4L2_TUNER_CAP_RDS_BLOCK_IO | V4L2_TUNER_CAP_LOW |
+ V4L2_TUNER_CAP_FREQ_BANDS,
+ .rangelow = 1400000, /* 87.5 MHz */
+ .rangehigh = 1728000, /* 108.0 MHz */
+ .modulation = V4L2_BAND_MODULATION_FM,
+ },
+};
+
+
+static int cadet_getstereo(struct cadet *dev)
+{
+ int ret = V4L2_TUNER_SUB_MONO;
+
+ if (!dev->is_fm_band) /* Only FM has stereo capability! */
+ return V4L2_TUNER_SUB_MONO;
+
+ outb(7, dev->io); /* Select tuner control */
+ if ((inb(dev->io + 1) & 0x40) == 0)
+ ret = V4L2_TUNER_SUB_STEREO;
+ return ret;
+}
+
+static unsigned cadet_gettune(struct cadet *dev)
+{
+ int curvol, i;
+ unsigned fifo = 0;
+
+ /*
+ * Prepare for read
+ */
+
+ outb(7, dev->io); /* Select tuner control */
+ curvol = inb(dev->io + 1); /* Save current volume/mute setting */
+ outb(0x00, dev->io + 1); /* Ensure WRITE-ENABLE is LOW */
+ dev->tunestat = 0xffff;
+
+ /*
+ * Read the shift register
+ */
+ for (i = 0; i < 25; i++) {
+ fifo = (fifo << 1) | ((inb(dev->io + 1) >> 7) & 0x01);
+ if (i < 24) {
+ outb(0x01, dev->io + 1);
+ dev->tunestat &= inb(dev->io + 1);
+ outb(0x00, dev->io + 1);
+ }
+ }
+
+ /*
+ * Restore volume/mute setting
+ */
+ outb(curvol, dev->io + 1);
+ return fifo;
+}
+
+static unsigned cadet_getfreq(struct cadet *dev)
+{
+ int i;
+ unsigned freq = 0, test, fifo = 0;
+
+ /*
+ * Read current tuning
+ */
+ fifo = cadet_gettune(dev);
+
+ /*
+ * Convert to actual frequency
+ */
+ if (!dev->is_fm_band) /* AM */
+ return ((fifo & 0x7fff) - 450) * 16;
+
+ 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) / 1000; /* Make it 1/16 kHz */
+ return freq;
+}
+
+static void cadet_settune(struct cadet *dev, unsigned fifo)
+{
+ int i;
+ unsigned test;
+
+ outb(7, dev->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, dev->io); /* Select tuner control */
+ outb(test, dev->io + 1); /* Initialize for write */
+ for (i = 0; i < 25; i++) {
+ test |= 0x01; /* Toggle SCK High */
+ outb(test, dev->io + 1);
+ test &= 0xfe; /* Toggle SCK Low */
+ outb(test, dev->io + 1);
+ fifo = fifo << 1; /* Prepare the next bit */
+ test = 0x1c | ((fifo >> 23) & 0x02);
+ outb(test, dev->io + 1);
+ }
+}
+
+static void cadet_setfreq(struct cadet *dev, unsigned freq)
+{
+ unsigned fifo;
+ int i, j, test;
+ int curvol;
+
+ freq = clamp(freq, bands[dev->is_fm_band].rangelow,
+ bands[dev->is_fm_band].rangehigh);
+ dev->curfreq = freq;
+ /*
+ * Formulate a fifo command
+ */
+ fifo = 0;
+ if (dev->is_fm_band) { /* FM */
+ test = 102400;
+ freq = freq / 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;
+ }
+ } else { /* AM */
+ fifo = (freq / 16) + 450; /* Make it kHz */
+ fifo |= 0x100000; /* Select AM Band */
+ }
+
+ /*
+ * Save current volume/mute setting
+ */
+
+ outb(7, dev->io); /* Select tuner control */
+ curvol = inb(dev->io + 1);
+
+ /*
+ * Tune the card
+ */
+ for (j = 3; j > -1; j--) {
+ cadet_settune(dev, fifo | (j << 16));
+
+ outb(7, dev->io); /* Select tuner control */
+ outb(curvol, dev->io + 1);
+
+ msleep(100);
+
+ cadet_gettune(dev);
+ if ((dev->tunestat & 0x40) == 0) { /* Tuned */
+ dev->sigstrength = sigtable[dev->is_fm_band][j];
+ goto reset_rds;
+ }
+ }
+ dev->sigstrength = 0;
+reset_rds:
+ outb(3, dev->io);
+ outb(inb(dev->io + 1) & 0x7f, dev->io + 1);
+}
+
+static bool cadet_has_rds_data(struct cadet *dev)
+{
+ bool result;
+
+ mutex_lock(&dev->lock);
+ result = dev->rdsin != dev->rdsout;
+ mutex_unlock(&dev->lock);
+ return result;
+}
+
+
+static void cadet_handler(struct timer_list *t)
+{
+ struct cadet *dev = timer_container_of(dev, t, readtimer);
+
+ /* Service the RDS fifo */
+ if (mutex_trylock(&dev->lock)) {
+ outb(0x3, dev->io); /* Select RDS Decoder Control */
+ if ((inb(dev->io + 1) & 0x20) != 0)
+ pr_err("cadet: RDS fifo overflow\n");
+ outb(0x80, dev->io); /* Select RDS fifo */
+
+ while ((inb(dev->io) & 0x80) != 0) {
+ dev->rdsbuf[dev->rdsin] = inb(dev->io + 1);
+ if (dev->rdsin + 1 != dev->rdsout)
+ dev->rdsin++;
+ }
+ mutex_unlock(&dev->lock);
+ }
+
+ /*
+ * Service pending read
+ */
+ if (cadet_has_rds_data(dev))
+ wake_up_interruptible(&dev->read_queue);
+
+ /*
+ * Clean up and exit
+ */
+ dev->readtimer.expires = jiffies + msecs_to_jiffies(50);
+ add_timer(&dev->readtimer);
+}
+
+static void cadet_start_rds(struct cadet *dev)
+{
+ dev->rdsstat = 1;
+ outb(0x80, dev->io); /* Select RDS fifo */
+ timer_setup(&dev->readtimer, cadet_handler, 0);
+ dev->readtimer.expires = jiffies + msecs_to_jiffies(50);
+ add_timer(&dev->readtimer);
+}
+
+static ssize_t cadet_read(struct file *file, char __user *data, size_t count, loff_t *ppos)
+{
+ struct cadet *dev = video_drvdata(file);
+ unsigned char readbuf[RDS_BUFFER];
+ int i = 0;
+
+ mutex_lock(&dev->lock);
+ if (dev->rdsstat == 0)
+ cadet_start_rds(dev);
+ mutex_unlock(&dev->lock);
+
+ if (!cadet_has_rds_data(dev) && (file->f_flags & O_NONBLOCK))
+ return -EWOULDBLOCK;
+ i = wait_event_interruptible(dev->read_queue, cadet_has_rds_data(dev));
+ if (i)
+ return i;
+
+ mutex_lock(&dev->lock);
+ while (i < count && dev->rdsin != dev->rdsout)
+ readbuf[i++] = dev->rdsbuf[dev->rdsout++];
+ mutex_unlock(&dev->lock);
+
+ if (i && copy_to_user(data, readbuf, i))
+ return -EFAULT;
+ return i;
+}
+
+
+static int vidioc_querycap(struct file *file, void *priv,
+ struct v4l2_capability *v)
+{
+ strscpy(v->driver, "ADS Cadet", sizeof(v->driver));
+ strscpy(v->card, "ADS Cadet", sizeof(v->card));
+ strscpy(v->bus_info, "ISA:radio-cadet", sizeof(v->bus_info));
+ return 0;
+}
+
+static int vidioc_g_tuner(struct file *file, void *priv,
+ struct v4l2_tuner *v)
+{
+ struct cadet *dev = video_drvdata(file);
+
+ if (v->index)
+ return -EINVAL;
+ v->type = V4L2_TUNER_RADIO;
+ strscpy(v->name, "Radio", sizeof(v->name));
+ v->capability = bands[0].capability | bands[1].capability;
+ v->rangelow = bands[0].rangelow; /* 520 kHz (start of AM band) */
+ v->rangehigh = bands[1].rangehigh; /* 108.0 MHz (end of FM band) */
+ if (dev->is_fm_band) {
+ v->rxsubchans = cadet_getstereo(dev);
+ outb(3, dev->io);
+ outb(inb(dev->io + 1) & 0x7f, dev->io + 1);
+ mdelay(100);
+ outb(3, dev->io);
+ if (inb(dev->io + 1) & 0x80)
+ v->rxsubchans |= V4L2_TUNER_SUB_RDS;
+ } else {
+ v->rangelow = 8320; /* 520 kHz */
+ v->rangehigh = 26400; /* 1650 kHz */
+ v->rxsubchans = V4L2_TUNER_SUB_MONO;
+ }
+ v->audmode = V4L2_TUNER_MODE_STEREO;
+ v->signal = dev->sigstrength; /* We might need to modify scaling of this */
+ return 0;
+}
+
+static int vidioc_s_tuner(struct file *file, void *priv,
+ const struct v4l2_tuner *v)
+{
+ return v->index ? -EINVAL : 0;
+}
+
+static int vidioc_enum_freq_bands(struct file *file, void *priv,
+ struct v4l2_frequency_band *band)
+{
+ if (band->tuner)
+ return -EINVAL;
+ if (band->index >= ARRAY_SIZE(bands))
+ return -EINVAL;
+ *band = bands[band->index];
+ return 0;
+}
+
+static int vidioc_g_frequency(struct file *file, void *priv,
+ struct v4l2_frequency *f)
+{
+ struct cadet *dev = video_drvdata(file);
+
+ if (f->tuner)
+ return -EINVAL;
+ f->type = V4L2_TUNER_RADIO;
+ f->frequency = dev->curfreq;
+ return 0;
+}
+
+
+static int vidioc_s_frequency(struct file *file, void *priv,
+ const struct v4l2_frequency *f)
+{
+ struct cadet *dev = video_drvdata(file);
+
+ if (f->tuner)
+ return -EINVAL;
+ dev->is_fm_band =
+ f->frequency >= (bands[0].rangehigh + bands[1].rangelow) / 2;
+ cadet_setfreq(dev, f->frequency);
+ return 0;
+}
+
+static int cadet_s_ctrl(struct v4l2_ctrl *ctrl)
+{
+ struct cadet *dev = container_of(ctrl->handler, struct cadet, ctrl_handler);
+
+ switch (ctrl->id) {
+ case V4L2_CID_AUDIO_MUTE:
+ outb(7, dev->io); /* Select tuner control */
+ if (ctrl->val)
+ outb(0x00, dev->io + 1);
+ else
+ outb(0x20, dev->io + 1);
+ return 0;
+ }
+ return -EINVAL;
+}
+
+static int cadet_open(struct file *file)
+{
+ struct cadet *dev = video_drvdata(file);
+ int err;
+
+ mutex_lock(&dev->lock);
+ err = v4l2_fh_open(file);
+ if (err)
+ goto fail;
+ if (v4l2_fh_is_singular_file(file))
+ init_waitqueue_head(&dev->read_queue);
+fail:
+ mutex_unlock(&dev->lock);
+ return err;
+}
+
+static int cadet_release(struct file *file)
+{
+ struct cadet *dev = video_drvdata(file);
+
+ mutex_lock(&dev->lock);
+ if (v4l2_fh_is_singular_file(file) && dev->rdsstat) {
+ timer_delete_sync(&dev->readtimer);
+ dev->rdsstat = 0;
+ }
+ v4l2_fh_release(file);
+ mutex_unlock(&dev->lock);
+ return 0;
+}
+
+static __poll_t cadet_poll(struct file *file, struct poll_table_struct *wait)
+{
+ struct cadet *dev = video_drvdata(file);
+ __poll_t req_events = poll_requested_events(wait);
+ __poll_t res = v4l2_ctrl_poll(file, wait);
+
+ poll_wait(file, &dev->read_queue, wait);
+ if (dev->rdsstat == 0 && (req_events & (EPOLLIN | EPOLLRDNORM))) {
+ mutex_lock(&dev->lock);
+ if (dev->rdsstat == 0)
+ cadet_start_rds(dev);
+ mutex_unlock(&dev->lock);
+ }
+ if (cadet_has_rds_data(dev))
+ res |= EPOLLIN | EPOLLRDNORM;
+ return res;
+}
+
+
+static const struct v4l2_file_operations cadet_fops = {
+ .owner = THIS_MODULE,
+ .open = cadet_open,
+ .release = cadet_release,
+ .read = cadet_read,
+ .unlocked_ioctl = video_ioctl2,
+ .poll = cadet_poll,
+};
+
+static const struct v4l2_ioctl_ops cadet_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_enum_freq_bands = vidioc_enum_freq_bands,
+ .vidioc_log_status = v4l2_ctrl_log_status,
+ .vidioc_subscribe_event = v4l2_ctrl_subscribe_event,
+ .vidioc_unsubscribe_event = v4l2_event_unsubscribe,
+};
+
+static const struct v4l2_ctrl_ops cadet_ctrl_ops = {
+ .s_ctrl = cadet_s_ctrl,
+};
+
+#ifdef CONFIG_PNP
+
+static const struct pnp_device_id cadet_pnp_devices[] = {
+ /* ADS Cadet AM/FM Radio Card */
+ { .id = "MSM0c24" },
+ { }
+};
+
+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(KERN_INFO "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,
+};
+
+#else
+static struct pnp_driver cadet_pnp_driver;
+#endif
+
+static void cadet_probe(struct cadet *dev)
+{
+ static int iovals[8] = { 0x330, 0x332, 0x334, 0x336, 0x338, 0x33a, 0x33c, 0x33e };
+ int i;
+
+ for (i = 0; i < 8; i++) {
+ dev->io = iovals[i];
+ if (request_region(dev->io, 2, "cadet-probe")) {
+ cadet_setfreq(dev, bands[1].rangelow);
+ if (cadet_getfreq(dev) == bands[1].rangelow) {
+ release_region(dev->io, 2);
+ return;
+ }
+ release_region(dev->io, 2);
+ }
+ }
+ dev->io = -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)
+{
+ struct cadet *dev = &cadet_card;
+ struct v4l2_device *v4l2_dev = &dev->v4l2_dev;
+ struct v4l2_ctrl_handler *hdl;
+ int res = -ENODEV;
+
+ strscpy(v4l2_dev->name, "cadet", sizeof(v4l2_dev->name));
+ mutex_init(&dev->lock);
+
+ /* If a probe was requested then probe ISAPnP first (safest) */
+ if (io < 0)
+ pnp_register_driver(&cadet_pnp_driver);
+ dev->io = io;
+
+ /* If that fails then probe unsafely if probe is requested */
+ if (dev->io < 0)
+ cadet_probe(dev);
+
+ /* Else we bail out */
+ if (dev->io < 0) {
+#ifdef MODULE
+ v4l2_err(v4l2_dev, "you must set an I/O address with io=0x330, 0x332, 0x334,\n");
+ v4l2_err(v4l2_dev, "0x336, 0x338, 0x33a, 0x33c or 0x33e\n");
+#endif
+ goto fail;
+ }
+ if (!request_region(dev->io, 2, "cadet"))
+ goto fail;
+
+ res = v4l2_device_register(NULL, v4l2_dev);
+ if (res < 0) {
+ release_region(dev->io, 2);
+ v4l2_err(v4l2_dev, "could not register v4l2_device\n");
+ goto fail;
+ }
+
+ hdl = &dev->ctrl_handler;
+ v4l2_ctrl_handler_init(hdl, 2);
+ v4l2_ctrl_new_std(hdl, &cadet_ctrl_ops,
+ V4L2_CID_AUDIO_MUTE, 0, 1, 1, 1);
+ v4l2_dev->ctrl_handler = hdl;
+ if (hdl->error) {
+ res = hdl->error;
+ v4l2_err(v4l2_dev, "Could not register controls\n");
+ goto err_hdl;
+ }
+
+ dev->is_fm_band = true;
+ dev->curfreq = bands[dev->is_fm_band].rangelow;
+ cadet_setfreq(dev, dev->curfreq);
+ strscpy(dev->vdev.name, v4l2_dev->name, sizeof(dev->vdev.name));
+ dev->vdev.v4l2_dev = v4l2_dev;
+ dev->vdev.fops = &cadet_fops;
+ dev->vdev.ioctl_ops = &cadet_ioctl_ops;
+ dev->vdev.release = video_device_release_empty;
+ dev->vdev.lock = &dev->lock;
+ dev->vdev.device_caps = V4L2_CAP_TUNER | V4L2_CAP_RADIO |
+ V4L2_CAP_READWRITE | V4L2_CAP_RDS_CAPTURE;
+ video_set_drvdata(&dev->vdev, dev);
+
+ res = video_register_device(&dev->vdev, VFL_TYPE_RADIO, radio_nr);
+ if (res < 0)
+ goto err_hdl;
+ v4l2_info(v4l2_dev, "ADS Cadet Radio Card at 0x%x\n", dev->io);
+ return 0;
+err_hdl:
+ v4l2_ctrl_handler_free(hdl);
+ v4l2_device_unregister(v4l2_dev);
+ release_region(dev->io, 2);
+fail:
+ pnp_unregister_driver(&cadet_pnp_driver);
+ return res;
+}
+
+static void __exit cadet_exit(void)
+{
+ struct cadet *dev = &cadet_card;
+
+ video_unregister_device(&dev->vdev);
+ v4l2_ctrl_handler_free(&dev->ctrl_handler);
+ v4l2_device_unregister(&dev->v4l2_dev);
+ outb(7, dev->io); /* Mute */
+ outb(0x00, dev->io + 1);
+ release_region(dev->io, 2);
+ pnp_unregister_driver(&cadet_pnp_driver);
+}
+
+module_init(cadet_init);
+module_exit(cadet_exit);
+
diff --git a/drivers/media/radio/radio-gemtek.c b/drivers/media/radio/radio-gemtek.c
new file mode 100644
index 000000000..7e4257e4b
--- /dev/null
+++ b/drivers/media/radio/radio-gemtek.c
@@ -0,0 +1,340 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * GemTek radio card driver
+ *
+ * Copyright 1998 Jonas Munsin <jmunsin@iki.fi>
+ *
+ * GemTek hasn't released any specs on the card, so the protocol had to
+ * be reverse engineered with dosemu.
+ *
+ * Besides the protocol changes, this is mostly a copy of:
+ *
+ * RadioTrack II driver for Linux radio support (C) 1998 Ben Pfaff
+ *
+ * Based on RadioTrack I/RadioReveal (C) 1997 M. Kirkwood
+ * Converted to new API by Alan Cox <alan@lxorguk.ukuu.org.uk>
+ * Various bugfixes and enhancements by Russell Kroll <rkroll@exploits.org>
+ *
+ * Converted to the radio-isa framework by Hans Verkuil <hverkuil@kernel.org>
+ * Converted to V4L2 API by Mauro Carvalho Chehab <mchehab@kernel.org>
+ *
+ * Note: this card seems to swap the left and right audio channels!
+ *
+ * Fully tested with the Keene USB FM Transmitter and the v4l2-compliance tool.
+ */
+
+#include <linux/module.h> /* Modules */
+#include <linux/init.h> /* Initdata */
+#include <linux/ioport.h> /* request_region */
+#include <linux/delay.h> /* udelay */
+#include <linux/videodev2.h> /* kernel radio structs */
+#include <linux/mutex.h>
+#include <linux/io.h> /* outb, outb_p */
+#include <linux/pnp.h>
+#include <linux/slab.h>
+#include <media/v4l2-ioctl.h>
+#include <media/v4l2-device.h>
+#include "radio-isa.h"
+
+/*
+ * Module info.
+ */
+
+MODULE_AUTHOR("Jonas Munsin, Pekka Seppänen <pexu@kapsi.fi>");
+MODULE_DESCRIPTION("A driver for the GemTek Radio card.");
+MODULE_LICENSE("GPL");
+MODULE_VERSION("1.0.0");
+
+/*
+ * Module params.
+ */
+
+#ifndef CONFIG_RADIO_GEMTEK_PORT
+#define CONFIG_RADIO_GEMTEK_PORT -1
+#endif
+#ifndef CONFIG_RADIO_GEMTEK_PROBE
+#define CONFIG_RADIO_GEMTEK_PROBE 1
+#endif
+
+#define GEMTEK_MAX 4
+
+static bool probe = CONFIG_RADIO_GEMTEK_PROBE;
+static bool hardmute;
+static int io[GEMTEK_MAX] = { [0] = CONFIG_RADIO_GEMTEK_PORT,
+ [1 ... (GEMTEK_MAX - 1)] = -1 };
+static int radio_nr[GEMTEK_MAX] = { [0 ... (GEMTEK_MAX - 1)] = -1 };
+
+module_param(probe, bool, 0444);
+MODULE_PARM_DESC(probe, "Enable automatic device probing.");
+
+module_param(hardmute, bool, 0644);
+MODULE_PARM_DESC(hardmute, "Enable 'hard muting' by shutting down PLL, may reduce static noise.");
+
+module_param_array(io, int, NULL, 0444);
+MODULE_PARM_DESC(io, "Force I/O ports for the GemTek Radio card if automatic probing is disabled or fails. The most common I/O ports are: 0x20c 0x30c, 0x24c or 0x34c (0x20c, 0x248 and 0x28c have been reported to work for the combined sound/radiocard).");
+
+module_param_array(radio_nr, int, NULL, 0444);
+MODULE_PARM_DESC(radio_nr, "Radio device numbers");
+
+/*
+ * Frequency calculation constants. Intermediate frequency 10.52 MHz (nominal
+ * value 10.7 MHz), reference divisor 6.39 kHz (nominal 6.25 kHz).
+ */
+#define FSCALE 8
+#define IF_OFFSET ((unsigned int)(10.52 * 16000 * (1<<FSCALE)))
+#define REF_FREQ ((unsigned int)(6.39 * 16 * (1<<FSCALE)))
+
+#define GEMTEK_CK 0x01 /* Clock signal */
+#define GEMTEK_DA 0x02 /* Serial data */
+#define GEMTEK_CE 0x04 /* Chip enable */
+#define GEMTEK_NS 0x08 /* No signal */
+#define GEMTEK_MT 0x10 /* Line mute */
+#define GEMTEK_STDF_3_125_KHZ 0x01 /* Standard frequency 3.125 kHz */
+#define GEMTEK_PLL_OFF 0x07 /* PLL off */
+
+#define BU2614_BUS_SIZE 32 /* BU2614 / BU2614FS bus size */
+
+#define SHORT_DELAY 5 /* usec */
+#define LONG_DELAY 75 /* usec */
+
+struct gemtek {
+ struct radio_isa_card isa;
+ bool muted;
+ u32 bu2614data;
+};
+
+#define BU2614_FREQ_BITS 16 /* D0..D15, Frequency data */
+#define BU2614_PORT_BITS 3 /* P0..P2, Output port control data */
+#define BU2614_VOID_BITS 4 /* unused */
+#define BU2614_FMES_BITS 1 /* CT, Frequency measurement beginning data */
+#define BU2614_STDF_BITS 3 /* R0..R2, Standard frequency data */
+#define BU2614_SWIN_BITS 1 /* S, Switch between FMIN / AMIN */
+#define BU2614_SWAL_BITS 1 /* PS, Swallow counter division (AMIN only)*/
+#define BU2614_VOID2_BITS 1 /* unused */
+#define BU2614_FMUN_BITS 1 /* GT, Frequency measurement time & unlock */
+#define BU2614_TEST_BITS 1 /* TS, Test data is input */
+
+#define BU2614_FREQ_SHIFT 0
+#define BU2614_PORT_SHIFT (BU2614_FREQ_BITS + BU2614_FREQ_SHIFT)
+#define BU2614_VOID_SHIFT (BU2614_PORT_BITS + BU2614_PORT_SHIFT)
+#define BU2614_FMES_SHIFT (BU2614_VOID_BITS + BU2614_VOID_SHIFT)
+#define BU2614_STDF_SHIFT (BU2614_FMES_BITS + BU2614_FMES_SHIFT)
+#define BU2614_SWIN_SHIFT (BU2614_STDF_BITS + BU2614_STDF_SHIFT)
+#define BU2614_SWAL_SHIFT (BU2614_SWIN_BITS + BU2614_SWIN_SHIFT)
+#define BU2614_VOID2_SHIFT (BU2614_SWAL_BITS + BU2614_SWAL_SHIFT)
+#define BU2614_FMUN_SHIFT (BU2614_VOID2_BITS + BU2614_VOID2_SHIFT)
+#define BU2614_TEST_SHIFT (BU2614_FMUN_BITS + BU2614_FMUN_SHIFT)
+
+#define MKMASK(field) (((1UL<<BU2614_##field##_BITS) - 1) << \
+ BU2614_##field##_SHIFT)
+#define BU2614_PORT_MASK MKMASK(PORT)
+#define BU2614_FREQ_MASK MKMASK(FREQ)
+#define BU2614_VOID_MASK MKMASK(VOID)
+#define BU2614_FMES_MASK MKMASK(FMES)
+#define BU2614_STDF_MASK MKMASK(STDF)
+#define BU2614_SWIN_MASK MKMASK(SWIN)
+#define BU2614_SWAL_MASK MKMASK(SWAL)
+#define BU2614_VOID2_MASK MKMASK(VOID2)
+#define BU2614_FMUN_MASK MKMASK(FMUN)
+#define BU2614_TEST_MASK MKMASK(TEST)
+
+/*
+ * Set data which will be sent to BU2614FS.
+ */
+#define gemtek_bu2614_set(dev, field, data) ((dev)->bu2614data = \
+ ((dev)->bu2614data & ~field##_MASK) | ((data) << field##_SHIFT))
+
+/*
+ * Transmit settings to BU2614FS over GemTek IC.
+ */
+static void gemtek_bu2614_transmit(struct gemtek *gt)
+{
+ struct radio_isa_card *isa = &gt->isa;
+ int i, bit, q, mute;
+
+ mute = gt->muted ? GEMTEK_MT : 0x00;
+
+ outb_p(mute | GEMTEK_CE | GEMTEK_DA | GEMTEK_CK, isa->io);
+ udelay(LONG_DELAY);
+
+ for (i = 0, q = gt->bu2614data; i < 32; i++, q >>= 1) {
+ bit = (q & 1) ? GEMTEK_DA : 0;
+ outb_p(mute | GEMTEK_CE | bit, isa->io);
+ udelay(SHORT_DELAY);
+ outb_p(mute | GEMTEK_CE | bit | GEMTEK_CK, isa->io);
+ udelay(SHORT_DELAY);
+ }
+
+ outb_p(mute | GEMTEK_DA | GEMTEK_CK, isa->io);
+ udelay(SHORT_DELAY);
+}
+
+/*
+ * Calculate divisor from FM-frequency for BU2614FS (3.125 KHz STDF expected).
+ */
+static unsigned long gemtek_convfreq(unsigned long freq)
+{
+ return ((freq << FSCALE) + IF_OFFSET + REF_FREQ / 2) / REF_FREQ;
+}
+
+static struct radio_isa_card *gemtek_alloc(void)
+{
+ struct gemtek *gt = kzalloc_obj(*gt);
+
+ if (gt)
+ gt->muted = true;
+ return gt ? &gt->isa : NULL;
+}
+
+/*
+ * Set FM-frequency.
+ */
+static int gemtek_s_frequency(struct radio_isa_card *isa, u32 freq)
+{
+ struct gemtek *gt = container_of(isa, struct gemtek, isa);
+
+ if (hardmute && gt->muted)
+ return 0;
+
+ gemtek_bu2614_set(gt, BU2614_PORT, 0);
+ gemtek_bu2614_set(gt, BU2614_FMES, 0);
+ gemtek_bu2614_set(gt, BU2614_SWIN, 0); /* FM-mode */
+ gemtek_bu2614_set(gt, BU2614_SWAL, 0);
+ gemtek_bu2614_set(gt, BU2614_FMUN, 1); /* GT bit set */
+ gemtek_bu2614_set(gt, BU2614_TEST, 0);
+ gemtek_bu2614_set(gt, BU2614_STDF, GEMTEK_STDF_3_125_KHZ);
+ gemtek_bu2614_set(gt, BU2614_FREQ, gemtek_convfreq(freq));
+ gemtek_bu2614_transmit(gt);
+ return 0;
+}
+
+/*
+ * Set mute flag.
+ */
+static int gemtek_s_mute_volume(struct radio_isa_card *isa, bool mute, int vol)
+{
+ struct gemtek *gt = container_of(isa, struct gemtek, isa);
+ int i;
+
+ gt->muted = mute;
+ if (hardmute) {
+ if (!mute)
+ return gemtek_s_frequency(isa, isa->freq);
+
+ /* Turn off PLL, disable data output */
+ gemtek_bu2614_set(gt, BU2614_PORT, 0);
+ gemtek_bu2614_set(gt, BU2614_FMES, 0); /* CT bit off */
+ gemtek_bu2614_set(gt, BU2614_SWIN, 0); /* FM-mode */
+ gemtek_bu2614_set(gt, BU2614_SWAL, 0);
+ gemtek_bu2614_set(gt, BU2614_FMUN, 0); /* GT bit off */
+ gemtek_bu2614_set(gt, BU2614_TEST, 0);
+ gemtek_bu2614_set(gt, BU2614_STDF, GEMTEK_PLL_OFF);
+ gemtek_bu2614_set(gt, BU2614_FREQ, 0);
+ gemtek_bu2614_transmit(gt);
+ return 0;
+ }
+
+ /* Read bus contents (CE, CK and DA). */
+ i = inb_p(isa->io);
+ /* Write it back with mute flag set. */
+ outb_p((i >> 5) | (mute ? GEMTEK_MT : 0), isa->io);
+ udelay(SHORT_DELAY);
+ return 0;
+}
+
+static u32 gemtek_g_rxsubchans(struct radio_isa_card *isa)
+{
+ if (inb_p(isa->io) & GEMTEK_NS)
+ return V4L2_TUNER_SUB_MONO;
+ return V4L2_TUNER_SUB_STEREO;
+}
+
+/*
+ * Check if requested card acts like GemTek Radio card.
+ */
+static bool gemtek_probe(struct radio_isa_card *isa, int io)
+{
+ int i, q;
+
+ q = inb_p(io); /* Read bus contents before probing. */
+ /* Try to turn on CE, CK and DA respectively and check if card responds
+ properly. */
+ for (i = 0; i < 3; ++i) {
+ outb_p(1 << i, io);
+ udelay(SHORT_DELAY);
+
+ if ((inb_p(io) & ~GEMTEK_NS) != (0x17 | (1 << (i + 5))))
+ return false;
+ }
+ outb_p(q >> 5, io); /* Write bus contents back. */
+ udelay(SHORT_DELAY);
+ return true;
+}
+
+static const struct radio_isa_ops gemtek_ops = {
+ .alloc = gemtek_alloc,
+ .probe = gemtek_probe,
+ .s_mute_volume = gemtek_s_mute_volume,
+ .s_frequency = gemtek_s_frequency,
+ .g_rxsubchans = gemtek_g_rxsubchans,
+};
+
+static const int gemtek_ioports[] = { 0x20c, 0x30c, 0x24c, 0x34c, 0x248, 0x28c };
+
+#ifdef CONFIG_PNP
+static const struct pnp_device_id gemtek_pnp_devices[] = {
+ /* AOpen FX-3D/Pro Radio */
+ { .id = "ADS7183" },
+ { }
+};
+
+MODULE_DEVICE_TABLE(pnp, gemtek_pnp_devices);
+#endif
+
+static struct radio_isa_driver gemtek_driver = {
+ .driver = {
+ .match = radio_isa_match,
+ .probe = radio_isa_probe,
+ .remove = radio_isa_remove,
+ .driver = {
+ .name = "radio-gemtek",
+ },
+ },
+#ifdef CONFIG_PNP
+ .pnp_driver = {
+ .name = "radio-gemtek",
+ .id_table = gemtek_pnp_devices,
+ .probe = radio_isa_pnp_probe,
+ .remove = radio_isa_pnp_remove,
+ },
+#endif
+ .io_params = io,
+ .radio_nr_params = radio_nr,
+ .io_ports = gemtek_ioports,
+ .num_of_io_ports = ARRAY_SIZE(gemtek_ioports),
+ .region_size = 1,
+ .card = "GemTek Radio",
+ .ops = &gemtek_ops,
+ .has_stereo = true,
+};
+
+static int __init gemtek_init(void)
+{
+ gemtek_driver.probe = probe;
+#ifdef CONFIG_PNP
+ pnp_register_driver(&gemtek_driver.pnp_driver);
+#endif
+ return isa_register_driver(&gemtek_driver.driver, GEMTEK_MAX);
+}
+
+static void __exit gemtek_exit(void)
+{
+ hardmute = true; /* Turn off PLL */
+#ifdef CONFIG_PNP
+ pnp_unregister_driver(&gemtek_driver.pnp_driver);
+#endif
+ isa_unregister_driver(&gemtek_driver.driver);
+}
+
+module_init(gemtek_init);
+module_exit(gemtek_exit);
diff --git a/drivers/media/radio/radio-isa.c b/drivers/media/radio/radio-isa.c
new file mode 100644
index 000000000..1a144536f
--- /dev/null
+++ b/drivers/media/radio/radio-isa.c
@@ -0,0 +1,377 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Framework for ISA radio drivers.
+ * This takes care of all the V4L2 scaffolding, allowing the ISA drivers
+ * to concentrate on the actual hardware operation.
+ *
+ * Copyright (C) 2012 Hans Verkuil <hverkuil@kernel.org>
+ */
+
+#include <linux/module.h>
+#include <linux/init.h>
+#include <linux/ioport.h>
+#include <linux/delay.h>
+#include <linux/videodev2.h>
+#include <linux/io.h>
+#include <linux/slab.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-ioctl.h>
+#include <media/v4l2-fh.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-event.h>
+
+#include "radio-isa.h"
+
+MODULE_AUTHOR("Hans Verkuil");
+MODULE_DESCRIPTION("A framework for ISA radio drivers.");
+MODULE_LICENSE("GPL");
+
+#define FREQ_LOW (87U * 16000U)
+#define FREQ_HIGH (108U * 16000U)
+
+static int radio_isa_querycap(struct file *file, void *priv,
+ struct v4l2_capability *v)
+{
+ struct radio_isa_card *isa = video_drvdata(file);
+
+ strscpy(v->driver, isa->drv->driver.driver.name, sizeof(v->driver));
+ strscpy(v->card, isa->drv->card, sizeof(v->card));
+ snprintf(v->bus_info, sizeof(v->bus_info), "ISA:%s", dev_name(isa->v4l2_dev.dev));
+ return 0;
+}
+
+static int radio_isa_g_tuner(struct file *file, void *priv,
+ struct v4l2_tuner *v)
+{
+ struct radio_isa_card *isa = video_drvdata(file);
+ const struct radio_isa_ops *ops = isa->drv->ops;
+
+ if (v->index > 0)
+ return -EINVAL;
+
+ strscpy(v->name, "FM", sizeof(v->name));
+ v->type = V4L2_TUNER_RADIO;
+ v->rangelow = FREQ_LOW;
+ v->rangehigh = FREQ_HIGH;
+ v->capability = V4L2_TUNER_CAP_LOW;
+ if (isa->drv->has_stereo)
+ v->capability |= V4L2_TUNER_CAP_STEREO;
+
+ if (ops->g_rxsubchans)
+ v->rxsubchans = ops->g_rxsubchans(isa);
+ else
+ v->rxsubchans = V4L2_TUNER_SUB_MONO | V4L2_TUNER_SUB_STEREO;
+ v->audmode = isa->stereo ? V4L2_TUNER_MODE_STEREO : V4L2_TUNER_MODE_MONO;
+ if (ops->g_signal)
+ v->signal = ops->g_signal(isa);
+ else
+ v->signal = (v->rxsubchans & V4L2_TUNER_SUB_STEREO) ?
+ 0xffff : 0;
+ return 0;
+}
+
+static int radio_isa_s_tuner(struct file *file, void *priv,
+ const struct v4l2_tuner *v)
+{
+ struct radio_isa_card *isa = video_drvdata(file);
+ const struct radio_isa_ops *ops = isa->drv->ops;
+
+ if (v->index)
+ return -EINVAL;
+ if (ops->s_stereo) {
+ isa->stereo = (v->audmode == V4L2_TUNER_MODE_STEREO);
+ return ops->s_stereo(isa, isa->stereo);
+ }
+ return 0;
+}
+
+static int radio_isa_s_frequency(struct file *file, void *priv,
+ const struct v4l2_frequency *f)
+{
+ struct radio_isa_card *isa = video_drvdata(file);
+ u32 freq = f->frequency;
+ int res;
+
+ if (f->tuner != 0 || f->type != V4L2_TUNER_RADIO)
+ return -EINVAL;
+ freq = clamp(freq, FREQ_LOW, FREQ_HIGH);
+ res = isa->drv->ops->s_frequency(isa, freq);
+ if (res == 0)
+ isa->freq = freq;
+ return res;
+}
+
+static int radio_isa_g_frequency(struct file *file, void *priv,
+ struct v4l2_frequency *f)
+{
+ struct radio_isa_card *isa = video_drvdata(file);
+
+ if (f->tuner != 0)
+ return -EINVAL;
+ f->type = V4L2_TUNER_RADIO;
+ f->frequency = isa->freq;
+ return 0;
+}
+
+static int radio_isa_s_ctrl(struct v4l2_ctrl *ctrl)
+{
+ struct radio_isa_card *isa =
+ container_of(ctrl->handler, struct radio_isa_card, hdl);
+
+ switch (ctrl->id) {
+ case V4L2_CID_AUDIO_MUTE:
+ return isa->drv->ops->s_mute_volume(isa, ctrl->val,
+ isa->volume ? isa->volume->val : 0);
+ }
+ return -EINVAL;
+}
+
+static int radio_isa_log_status(struct file *file, void *priv)
+{
+ struct radio_isa_card *isa = video_drvdata(file);
+
+ v4l2_info(&isa->v4l2_dev, "I/O Port = 0x%03x\n", isa->io);
+ v4l2_ctrl_handler_log_status(&isa->hdl, isa->v4l2_dev.name);
+ return 0;
+}
+
+static const struct v4l2_ctrl_ops radio_isa_ctrl_ops = {
+ .s_ctrl = radio_isa_s_ctrl,
+};
+
+static const struct v4l2_file_operations radio_isa_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 radio_isa_ioctl_ops = {
+ .vidioc_querycap = radio_isa_querycap,
+ .vidioc_g_tuner = radio_isa_g_tuner,
+ .vidioc_s_tuner = radio_isa_s_tuner,
+ .vidioc_g_frequency = radio_isa_g_frequency,
+ .vidioc_s_frequency = radio_isa_s_frequency,
+ .vidioc_log_status = radio_isa_log_status,
+ .vidioc_subscribe_event = v4l2_ctrl_subscribe_event,
+ .vidioc_unsubscribe_event = v4l2_event_unsubscribe,
+};
+
+int radio_isa_match(struct device *pdev, unsigned int dev)
+{
+ struct radio_isa_driver *drv = pdev->platform_data;
+
+ return drv->probe || drv->io_params[dev] >= 0;
+}
+EXPORT_SYMBOL_GPL(radio_isa_match);
+
+static bool radio_isa_valid_io(const struct radio_isa_driver *drv, int io)
+{
+ int i;
+
+ for (i = 0; i < drv->num_of_io_ports; i++)
+ if (drv->io_ports[i] == io)
+ return true;
+ return false;
+}
+
+static struct radio_isa_card *radio_isa_alloc(struct radio_isa_driver *drv,
+ struct device *pdev)
+{
+ struct v4l2_device *v4l2_dev;
+ struct radio_isa_card *isa = drv->ops->alloc();
+ if (!isa)
+ return NULL;
+
+ dev_set_drvdata(pdev, isa);
+ isa->drv = drv;
+ v4l2_dev = &isa->v4l2_dev;
+ strscpy(v4l2_dev->name, dev_name(pdev), sizeof(v4l2_dev->name));
+
+ return isa;
+}
+
+static int radio_isa_common_probe(struct radio_isa_card *isa,
+ struct device *pdev,
+ int radio_nr, unsigned region_size)
+{
+ const struct radio_isa_driver *drv = isa->drv;
+ const struct radio_isa_ops *ops = drv->ops;
+ struct v4l2_device *v4l2_dev = &isa->v4l2_dev;
+ int res;
+
+ if (!request_region(isa->io, region_size, v4l2_dev->name)) {
+ v4l2_err(v4l2_dev, "port 0x%x already in use\n", isa->io);
+ kfree(isa);
+ return -EBUSY;
+ }
+
+ res = v4l2_device_register(pdev, v4l2_dev);
+ if (res < 0) {
+ v4l2_err(v4l2_dev, "Could not register v4l2_device\n");
+ goto err_dev_reg;
+ }
+
+ v4l2_ctrl_handler_init(&isa->hdl, 1);
+ isa->mute = v4l2_ctrl_new_std(&isa->hdl, &radio_isa_ctrl_ops,
+ V4L2_CID_AUDIO_MUTE, 0, 1, 1, 1);
+ if (drv->max_volume)
+ isa->volume = v4l2_ctrl_new_std(&isa->hdl, &radio_isa_ctrl_ops,
+ V4L2_CID_AUDIO_VOLUME, 0, drv->max_volume, 1,
+ drv->max_volume);
+ v4l2_dev->ctrl_handler = &isa->hdl;
+ if (isa->hdl.error) {
+ res = isa->hdl.error;
+ v4l2_err(v4l2_dev, "Could not register controls\n");
+ goto err_hdl;
+ }
+ if (drv->max_volume)
+ v4l2_ctrl_cluster(2, &isa->mute);
+ v4l2_dev->ctrl_handler = &isa->hdl;
+
+ mutex_init(&isa->lock);
+ isa->vdev.lock = &isa->lock;
+ strscpy(isa->vdev.name, v4l2_dev->name, sizeof(isa->vdev.name));
+ isa->vdev.v4l2_dev = v4l2_dev;
+ isa->vdev.fops = &radio_isa_fops;
+ isa->vdev.ioctl_ops = &radio_isa_ioctl_ops;
+ isa->vdev.release = video_device_release_empty;
+ isa->vdev.device_caps = V4L2_CAP_TUNER | V4L2_CAP_RADIO;
+ video_set_drvdata(&isa->vdev, isa);
+ isa->freq = FREQ_LOW;
+ isa->stereo = drv->has_stereo;
+
+ if (ops->init)
+ res = ops->init(isa);
+ if (!res)
+ res = v4l2_ctrl_handler_setup(&isa->hdl);
+ if (!res)
+ res = ops->s_frequency(isa, isa->freq);
+ if (!res && ops->s_stereo)
+ res = ops->s_stereo(isa, isa->stereo);
+ if (res < 0) {
+ v4l2_err(v4l2_dev, "Could not setup card\n");
+ goto err_hdl;
+ }
+ res = video_register_device(&isa->vdev, VFL_TYPE_RADIO, radio_nr);
+
+ if (res < 0) {
+ v4l2_err(v4l2_dev, "Could not register device node\n");
+ goto err_hdl;
+ }
+
+ v4l2_info(v4l2_dev, "Initialized radio card %s on port 0x%03x\n",
+ drv->card, isa->io);
+ return 0;
+
+err_hdl:
+ v4l2_ctrl_handler_free(&isa->hdl);
+err_dev_reg:
+ release_region(isa->io, region_size);
+ kfree(isa);
+ return res;
+}
+
+static void radio_isa_common_remove(struct radio_isa_card *isa,
+ unsigned region_size)
+{
+ const struct radio_isa_ops *ops = isa->drv->ops;
+
+ ops->s_mute_volume(isa, true, isa->volume ? isa->volume->cur.val : 0);
+ video_unregister_device(&isa->vdev);
+ v4l2_ctrl_handler_free(&isa->hdl);
+ v4l2_device_unregister(&isa->v4l2_dev);
+ release_region(isa->io, region_size);
+ v4l2_info(&isa->v4l2_dev, "Removed radio card %s\n", isa->drv->card);
+ kfree(isa);
+}
+
+int radio_isa_probe(struct device *pdev, unsigned int dev)
+{
+ struct radio_isa_driver *drv = pdev->platform_data;
+ const struct radio_isa_ops *ops = drv->ops;
+ struct v4l2_device *v4l2_dev;
+ struct radio_isa_card *isa;
+
+ isa = radio_isa_alloc(drv, pdev);
+ if (!isa)
+ return -ENOMEM;
+ isa->io = drv->io_params[dev];
+ v4l2_dev = &isa->v4l2_dev;
+
+ if (drv->probe && ops->probe) {
+ int i;
+
+ for (i = 0; i < drv->num_of_io_ports; ++i) {
+ int io = drv->io_ports[i];
+
+ if (request_region(io, drv->region_size, v4l2_dev->name)) {
+ bool found = ops->probe(isa, io);
+
+ release_region(io, drv->region_size);
+ if (found) {
+ isa->io = io;
+ break;
+ }
+ }
+ }
+ }
+
+ if (!radio_isa_valid_io(drv, isa->io)) {
+ int i;
+
+ if (isa->io < 0)
+ return -ENODEV;
+ v4l2_err(v4l2_dev, "you must set an I/O address with io=0x%03x",
+ drv->io_ports[0]);
+ for (i = 1; i < drv->num_of_io_ports; i++)
+ printk(KERN_CONT "/0x%03x", drv->io_ports[i]);
+ printk(KERN_CONT ".\n");
+ kfree(isa);
+ return -EINVAL;
+ }
+
+ return radio_isa_common_probe(isa, pdev, drv->radio_nr_params[dev],
+ drv->region_size);
+}
+EXPORT_SYMBOL_GPL(radio_isa_probe);
+
+void radio_isa_remove(struct device *pdev, unsigned int dev)
+{
+ struct radio_isa_card *isa = dev_get_drvdata(pdev);
+
+ radio_isa_common_remove(isa, isa->drv->region_size);
+}
+EXPORT_SYMBOL_GPL(radio_isa_remove);
+
+#ifdef CONFIG_PNP
+int radio_isa_pnp_probe(struct pnp_dev *dev, const struct pnp_device_id *dev_id)
+{
+ struct pnp_driver *pnp_drv = to_pnp_driver(dev->dev.driver);
+ struct radio_isa_driver *drv = container_of(pnp_drv,
+ struct radio_isa_driver, pnp_driver);
+ struct radio_isa_card *isa;
+
+ if (!pnp_port_valid(dev, 0))
+ return -ENODEV;
+
+ isa = radio_isa_alloc(drv, &dev->dev);
+ if (!isa)
+ return -ENOMEM;
+
+ isa->io = pnp_port_start(dev, 0);
+
+ return radio_isa_common_probe(isa, &dev->dev, drv->radio_nr_params[0],
+ pnp_port_len(dev, 0));
+}
+EXPORT_SYMBOL_GPL(radio_isa_pnp_probe);
+
+void radio_isa_pnp_remove(struct pnp_dev *dev)
+{
+ struct radio_isa_card *isa = dev_get_drvdata(&dev->dev);
+
+ radio_isa_common_remove(isa, pnp_port_len(dev, 0));
+}
+EXPORT_SYMBOL_GPL(radio_isa_pnp_remove);
+#endif
diff --git a/drivers/media/radio/radio-isa.h b/drivers/media/radio/radio-isa.h
new file mode 100644
index 000000000..62ff5c3fb
--- /dev/null
+++ b/drivers/media/radio/radio-isa.h
@@ -0,0 +1,101 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Framework for ISA radio drivers.
+ * This takes care of all the V4L2 scaffolding, allowing the ISA drivers
+ * to concentrate on the actual hardware operation.
+ *
+ * Copyright (C) 2012 Hans Verkuil <hverkuil@kernel.org>
+ */
+
+#ifndef _RADIO_ISA_H_
+#define _RADIO_ISA_H_
+
+#include <linux/isa.h>
+#include <linux/pnp.h>
+#include <linux/videodev2.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-ctrls.h>
+
+struct radio_isa_driver;
+struct radio_isa_ops;
+
+/* Core structure for radio ISA cards */
+struct radio_isa_card {
+ const struct radio_isa_driver *drv;
+ struct v4l2_device v4l2_dev;
+ struct v4l2_ctrl_handler hdl;
+ struct video_device vdev;
+ struct mutex lock;
+ const struct radio_isa_ops *ops;
+ struct { /* mute/volume cluster */
+ struct v4l2_ctrl *mute;
+ struct v4l2_ctrl *volume;
+ };
+ /* I/O port */
+ int io;
+
+ /* Card is in stereo audio mode */
+ bool stereo;
+ /* Current frequency */
+ u32 freq;
+};
+
+struct radio_isa_ops {
+ /* Allocate and initialize a radio_isa_card struct */
+ struct radio_isa_card *(*alloc)(void);
+ /* Probe whether a card is present at the given port */
+ bool (*probe)(struct radio_isa_card *isa, int io);
+ /* Special card initialization can be done here, this is called after
+ * the standard controls are registered, but before they are setup,
+ * thus allowing drivers to add their own controls here. */
+ int (*init)(struct radio_isa_card *isa);
+ /* Set mute and volume. */
+ int (*s_mute_volume)(struct radio_isa_card *isa, bool mute, int volume);
+ /* Set frequency */
+ int (*s_frequency)(struct radio_isa_card *isa, u32 freq);
+ /* Set stereo/mono audio mode */
+ int (*s_stereo)(struct radio_isa_card *isa, bool stereo);
+ /* Get rxsubchans value for VIDIOC_G_TUNER */
+ u32 (*g_rxsubchans)(struct radio_isa_card *isa);
+ /* Get the signal strength for VIDIOC_G_TUNER */
+ u32 (*g_signal)(struct radio_isa_card *isa);
+};
+
+/* Top level structure needed to instantiate the cards */
+struct radio_isa_driver {
+ struct isa_driver driver;
+#ifdef CONFIG_PNP
+ struct pnp_driver pnp_driver;
+#endif
+ const struct radio_isa_ops *ops;
+ /* The module_param_array with the specified I/O ports */
+ int *io_params;
+ /* The module_param_array with the radio_nr values */
+ int *radio_nr_params;
+ /* Whether we should probe for possible cards */
+ bool probe;
+ /* The list of possible I/O ports */
+ const int *io_ports;
+ /* The size of that list */
+ int num_of_io_ports;
+ /* The region size to request */
+ unsigned region_size;
+ /* The name of the card */
+ const char *card;
+ /* Card can capture stereo audio */
+ bool has_stereo;
+ /* The maximum volume for the volume control. If 0, then there
+ is no volume control possible. */
+ int max_volume;
+};
+
+int radio_isa_match(struct device *pdev, unsigned int dev);
+int radio_isa_probe(struct device *pdev, unsigned int dev);
+void radio_isa_remove(struct device *pdev, unsigned int dev);
+#ifdef CONFIG_PNP
+int radio_isa_pnp_probe(struct pnp_dev *dev,
+ const struct pnp_device_id *dev_id);
+void radio_isa_pnp_remove(struct pnp_dev *dev);
+#endif
+
+#endif
diff --git a/drivers/media/radio/radio-keene.c b/drivers/media/radio/radio-keene.c
new file mode 100644
index 000000000..8cfef3a77
--- /dev/null
+++ b/drivers/media/radio/radio-keene.c
@@ -0,0 +1,405 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2012 Hans Verkuil <hverkuil@kernel.org>
+ */
+
+/* kernel includes */
+#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>
+
+/* driver and module definitions */
+MODULE_AUTHOR("Hans Verkuil <hverkuil@kernel.org>");
+MODULE_DESCRIPTION("Keene FM Transmitter driver");
+MODULE_LICENSE("GPL");
+
+/* Actually, it advertises itself as a Logitech */
+#define USB_KEENE_VENDOR 0x046d
+#define USB_KEENE_PRODUCT 0x0a0e
+
+/* Probably USB_TIMEOUT should be modified in module parameter */
+#define BUFFER_LENGTH 8
+#define USB_TIMEOUT 500
+
+/* Frequency limits in MHz */
+#define FREQ_MIN 76U
+#define FREQ_MAX 108U
+#define FREQ_MUL 16000U
+
+/* USB Device ID List */
+static const struct usb_device_id usb_keene_device_table[] = {
+ {USB_DEVICE_AND_INTERFACE_INFO(USB_KEENE_VENDOR, USB_KEENE_PRODUCT,
+ USB_CLASS_HID, 0, 0) },
+ { } /* Terminating entry */
+};
+
+MODULE_DEVICE_TABLE(usb, usb_keene_device_table);
+
+struct keene_device {
+ struct usb_device *usbdev;
+ struct usb_interface *intf;
+ struct video_device vdev;
+ struct v4l2_device v4l2_dev;
+ struct v4l2_ctrl_handler hdl;
+ struct mutex lock;
+
+ u8 *buffer;
+ unsigned curfreq;
+ u8 tx;
+ u8 pa;
+ bool stereo;
+ bool muted;
+ bool preemph_75_us;
+};
+
+static inline struct keene_device *to_keene_dev(struct v4l2_device *v4l2_dev)
+{
+ return container_of(v4l2_dev, struct keene_device, v4l2_dev);
+}
+
+/* Set frequency (if non-0), PA, mute and turn on/off the FM transmitter. */
+static int keene_cmd_main(struct keene_device *radio, unsigned freq, bool play)
+{
+ unsigned short freq_send = freq ? (freq - 76 * 16000) / 800 : 0;
+ int ret;
+
+ radio->buffer[0] = 0x00;
+ radio->buffer[1] = 0x50;
+ radio->buffer[2] = (freq_send >> 8) & 0xff;
+ radio->buffer[3] = freq_send & 0xff;
+ radio->buffer[4] = radio->pa;
+ /* If bit 4 is set, then tune to the frequency.
+ If bit 3 is set, then unmute; if bit 2 is set, then mute.
+ If bit 1 is set, then enter idle mode; if bit 0 is set,
+ then enter transmit mode.
+ */
+ radio->buffer[5] = (radio->muted ? 4 : 8) | (play ? 1 : 2) |
+ (freq ? 0x10 : 0);
+ radio->buffer[6] = 0x00;
+ radio->buffer[7] = 0x00;
+
+ ret = usb_control_msg(radio->usbdev, usb_sndctrlpipe(radio->usbdev, 0),
+ 9, 0x21, 0x200, 2, radio->buffer, BUFFER_LENGTH, USB_TIMEOUT);
+
+ if (ret < 0) {
+ dev_warn(&radio->vdev.dev, "%s failed (%d)\n", __func__, ret);
+ return ret;
+ }
+ if (freq)
+ radio->curfreq = freq;
+ return 0;
+}
+
+/* Set TX, stereo and preemphasis mode (50 us vs 75 us). */
+static int keene_cmd_set(struct keene_device *radio)
+{
+ int ret;
+
+ radio->buffer[0] = 0x00;
+ radio->buffer[1] = 0x51;
+ radio->buffer[2] = radio->tx;
+ /* If bit 0 is set, then transmit mono, otherwise stereo.
+ If bit 2 is set, then enable 75 us preemphasis, otherwise
+ it is 50 us. */
+ radio->buffer[3] = (radio->stereo ? 0 : 1) | (radio->preemph_75_us ? 4 : 0);
+ radio->buffer[4] = 0x00;
+ radio->buffer[5] = 0x00;
+ radio->buffer[6] = 0x00;
+ radio->buffer[7] = 0x00;
+
+ ret = usb_control_msg(radio->usbdev, usb_sndctrlpipe(radio->usbdev, 0),
+ 9, 0x21, 0x200, 2, radio->buffer, BUFFER_LENGTH, USB_TIMEOUT);
+
+ if (ret < 0) {
+ dev_warn(&radio->vdev.dev, "%s failed (%d)\n", __func__, ret);
+ return ret;
+ }
+ return 0;
+}
+
+/* Handle unplugging the device.
+ * We call video_unregister_device in any case.
+ * The last function called in this procedure is
+ * usb_keene_device_release.
+ */
+static void usb_keene_disconnect(struct usb_interface *intf)
+{
+ struct keene_device *radio = to_keene_dev(usb_get_intfdata(intf));
+
+ mutex_lock(&radio->lock);
+ usb_set_intfdata(intf, NULL);
+ video_unregister_device(&radio->vdev);
+ v4l2_device_disconnect(&radio->v4l2_dev);
+ mutex_unlock(&radio->lock);
+ v4l2_device_put(&radio->v4l2_dev);
+}
+
+static int usb_keene_suspend(struct usb_interface *intf, pm_message_t message)
+{
+ struct keene_device *radio = to_keene_dev(usb_get_intfdata(intf));
+
+ return keene_cmd_main(radio, 0, false);
+}
+
+static int usb_keene_resume(struct usb_interface *intf)
+{
+ struct keene_device *radio = to_keene_dev(usb_get_intfdata(intf));
+
+ mdelay(50);
+ keene_cmd_set(radio);
+ keene_cmd_main(radio, radio->curfreq, true);
+ return 0;
+}
+
+static int vidioc_querycap(struct file *file, void *priv,
+ struct v4l2_capability *v)
+{
+ struct keene_device *radio = video_drvdata(file);
+
+ strscpy(v->driver, "radio-keene", sizeof(v->driver));
+ strscpy(v->card, "Keene FM Transmitter", sizeof(v->card));
+ usb_make_path(radio->usbdev, v->bus_info, sizeof(v->bus_info));
+ return 0;
+}
+
+static int vidioc_g_modulator(struct file *file, void *priv,
+ struct v4l2_modulator *v)
+{
+ struct keene_device *radio = video_drvdata(file);
+
+ if (v->index > 0)
+ return -EINVAL;
+
+ strscpy(v->name, "FM", sizeof(v->name));
+ v->rangelow = FREQ_MIN * FREQ_MUL;
+ v->rangehigh = FREQ_MAX * FREQ_MUL;
+ v->txsubchans = radio->stereo ? V4L2_TUNER_SUB_STEREO : V4L2_TUNER_SUB_MONO;
+ v->capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO;
+ return 0;
+}
+
+static int vidioc_s_modulator(struct file *file, void *priv,
+ const struct v4l2_modulator *v)
+{
+ struct keene_device *radio = video_drvdata(file);
+
+ if (v->index > 0)
+ return -EINVAL;
+
+ radio->stereo = (v->txsubchans == V4L2_TUNER_SUB_STEREO);
+ return keene_cmd_set(radio);
+}
+
+static int vidioc_s_frequency(struct file *file, void *priv,
+ const struct v4l2_frequency *f)
+{
+ struct keene_device *radio = video_drvdata(file);
+ unsigned freq = f->frequency;
+
+ if (f->tuner != 0 || f->type != V4L2_TUNER_RADIO)
+ return -EINVAL;
+ freq = clamp(freq, FREQ_MIN * FREQ_MUL, FREQ_MAX * FREQ_MUL);
+ return keene_cmd_main(radio, freq, true);
+}
+
+static int vidioc_g_frequency(struct file *file, void *priv,
+ struct v4l2_frequency *f)
+{
+ struct keene_device *radio = video_drvdata(file);
+
+ if (f->tuner != 0)
+ return -EINVAL;
+ f->type = V4L2_TUNER_RADIO;
+ f->frequency = radio->curfreq;
+ return 0;
+}
+
+static int keene_s_ctrl(struct v4l2_ctrl *ctrl)
+{
+ static const u8 db2tx[] = {
+ /* -15, -12, -9, -6, -3, 0 dB */
+ 0x03, 0x13, 0x02, 0x12, 0x22, 0x32,
+ /* 3, 6, 9, 12, 15, 18 dB */
+ 0x21, 0x31, 0x20, 0x30, 0x40, 0x50
+ };
+ struct keene_device *radio =
+ container_of(ctrl->handler, struct keene_device, hdl);
+
+ switch (ctrl->id) {
+ case V4L2_CID_AUDIO_MUTE:
+ radio->muted = ctrl->val;
+ return keene_cmd_main(radio, 0, true);
+
+ case V4L2_CID_TUNE_POWER_LEVEL:
+ /* To go from dBuV to the register value we apply the
+ following formula: */
+ radio->pa = (ctrl->val - 71) * 100 / 62;
+ return keene_cmd_main(radio, 0, true);
+
+ case V4L2_CID_TUNE_PREEMPHASIS:
+ radio->preemph_75_us = ctrl->val == V4L2_PREEMPHASIS_75_uS;
+ return keene_cmd_set(radio);
+
+ case V4L2_CID_AUDIO_COMPRESSION_GAIN:
+ radio->tx = db2tx[(ctrl->val - (s32)ctrl->minimum) / (s32)ctrl->step];
+ return keene_cmd_set(radio);
+ }
+ return -EINVAL;
+}
+
+/* File system interface */
+static const struct v4l2_file_operations usb_keene_fops = {
+ .owner = THIS_MODULE,
+ .open = v4l2_fh_open,
+ .release = v4l2_fh_release,
+ .poll = v4l2_ctrl_poll,
+ .unlocked_ioctl = video_ioctl2,
+};
+
+static const struct v4l2_ctrl_ops keene_ctrl_ops = {
+ .s_ctrl = keene_s_ctrl,
+};
+
+static const struct v4l2_ioctl_ops usb_keene_ioctl_ops = {
+ .vidioc_querycap = vidioc_querycap,
+ .vidioc_g_modulator = vidioc_g_modulator,
+ .vidioc_s_modulator = vidioc_s_modulator,
+ .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_keene_video_device_release(struct v4l2_device *v4l2_dev)
+{
+ struct keene_device *radio = to_keene_dev(v4l2_dev);
+
+ /* free rest memory */
+ v4l2_ctrl_handler_free(&radio->hdl);
+ kfree(radio->buffer);
+ kfree(radio);
+}
+
+/* check if the device is present and register with v4l and usb if it is */
+static int usb_keene_probe(struct usb_interface *intf,
+ const struct usb_device_id *id)
+{
+ struct usb_device *dev = interface_to_usbdev(intf);
+ struct keene_device *radio;
+ struct v4l2_ctrl_handler *hdl;
+ int retval = 0;
+
+ /*
+ * The Keene FM transmitter USB device has the same USB ID as
+ * the Logitech AudioHub Speaker, but it should ignore the hid.
+ * Check if the name is that of the Keene device.
+ * If not, then someone connected the AudioHub and we shouldn't
+ * attempt to handle this driver.
+ * For reference: the product name of the AudioHub is
+ * "AudioHub Speaker".
+ */
+ if (dev->product && strcmp(dev->product, "B-LINK USB Audio "))
+ return -ENODEV;
+
+ radio = kzalloc_obj(struct keene_device);
+ if (radio)
+ radio->buffer = kmalloc(BUFFER_LENGTH, GFP_KERNEL);
+
+ if (!radio || !radio->buffer) {
+ dev_err(&intf->dev, "kmalloc for keene_device failed\n");
+ kfree(radio);
+ retval = -ENOMEM;
+ goto err;
+ }
+
+ hdl = &radio->hdl;
+ v4l2_ctrl_handler_init(hdl, 4);
+ v4l2_ctrl_new_std(hdl, &keene_ctrl_ops, V4L2_CID_AUDIO_MUTE,
+ 0, 1, 1, 0);
+ v4l2_ctrl_new_std_menu(hdl, &keene_ctrl_ops, V4L2_CID_TUNE_PREEMPHASIS,
+ V4L2_PREEMPHASIS_75_uS, 1, V4L2_PREEMPHASIS_50_uS);
+ v4l2_ctrl_new_std(hdl, &keene_ctrl_ops, V4L2_CID_TUNE_POWER_LEVEL,
+ 84, 118, 1, 118);
+ v4l2_ctrl_new_std(hdl, &keene_ctrl_ops, V4L2_CID_AUDIO_COMPRESSION_GAIN,
+ -15, 18, 3, 0);
+ radio->pa = 118;
+ radio->tx = 0x32;
+ radio->stereo = true;
+ if (hdl->error) {
+ retval = hdl->error;
+
+ goto err_v4l2;
+ }
+ 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;
+ }
+
+ mutex_init(&radio->lock);
+
+ radio->v4l2_dev.ctrl_handler = hdl;
+ radio->v4l2_dev.release = usb_keene_video_device_release;
+ strscpy(radio->vdev.name, radio->v4l2_dev.name,
+ sizeof(radio->vdev.name));
+ radio->vdev.v4l2_dev = &radio->v4l2_dev;
+ radio->vdev.fops = &usb_keene_fops;
+ radio->vdev.ioctl_ops = &usb_keene_ioctl_ops;
+ radio->vdev.lock = &radio->lock;
+ radio->vdev.release = video_device_release_empty;
+ radio->vdev.vfl_dir = VFL_DIR_TX;
+ radio->vdev.device_caps = V4L2_CAP_RADIO | V4L2_CAP_MODULATOR;
+
+ radio->usbdev = interface_to_usbdev(intf);
+ radio->intf = intf;
+ usb_set_intfdata(intf, &radio->v4l2_dev);
+
+ video_set_drvdata(&radio->vdev, radio);
+
+ /* at least 11ms is needed in order to settle hardware */
+ msleep(20);
+ keene_cmd_main(radio, 95.16 * FREQ_MUL, false);
+
+ retval = video_register_device(&radio->vdev, VFL_TYPE_RADIO, -1);
+ if (retval < 0) {
+ dev_err(&intf->dev, "could not register video device\n");
+ goto err_vdev;
+ }
+ v4l2_ctrl_handler_setup(hdl);
+ dev_info(&intf->dev, "V4L2 device registered as %s\n",
+ video_device_node_name(&radio->vdev));
+ return 0;
+
+err_vdev:
+ v4l2_device_unregister(&radio->v4l2_dev);
+err_v4l2:
+ v4l2_ctrl_handler_free(&radio->hdl);
+ kfree(radio->buffer);
+ kfree(radio);
+err:
+ return retval;
+}
+
+/* USB subsystem interface */
+static struct usb_driver usb_keene_driver = {
+ .name = "radio-keene",
+ .probe = usb_keene_probe,
+ .disconnect = usb_keene_disconnect,
+ .id_table = usb_keene_device_table,
+ .suspend = usb_keene_suspend,
+ .resume = usb_keene_resume,
+ .reset_resume = usb_keene_resume,
+};
+
+module_usb_driver(usb_keene_driver);
+
diff --git a/drivers/media/radio/radio-ma901.c b/drivers/media/radio/radio-ma901.c
new file mode 100644
index 000000000..11c5d103c
--- /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_obj(struct ma901radio_device);
+ 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);
diff --git a/drivers/media/radio/radio-maxiradio.c b/drivers/media/radio/radio-maxiradio.c
new file mode 100644
index 000000000..8ab1dacf8
--- /dev/null
+++ b/drivers/media/radio/radio-maxiradio.c
@@ -0,0 +1,203 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Guillemot Maxi Radio FM 2000 PCI radio card driver for Linux
+ * (C) 2001 Dimitromanolakis Apostolos <apdim@grecian.net>
+ *
+ * Based in the radio Maestro PCI driver. Actually it uses the same chip
+ * for radio but different pci controller.
+ *
+ * I didn't have any specs I reversed engineered the protocol from
+ * the windows driver (radio.dll).
+ *
+ * The card uses the TEA5757 chip that includes a search function but it
+ * is useless as I haven't found any way to read back the frequency. If
+ * anybody does please mail me.
+ *
+ * For the pdf file see:
+ * http://www.nxp.com/acrobat_download2/expired_datasheets/TEA5757_5759_3.pdf
+ *
+ *
+ * CHANGES:
+ * 0.75b
+ * - better pci interface thanks to Francois Romieu <romieu@cogenit.fr>
+ *
+ * 0.75 Sun Feb 4 22:51:27 EET 2001
+ * - tiding up
+ * - removed support for multiple devices as it didn't work anyway
+ *
+ * BUGS:
+ * - card unmutes if you change frequency
+ *
+ * (c) 2006, 2007 by Mauro Carvalho Chehab <mchehab@kernel.org>:
+ * - Conversion to V4L2 API
+ * - Uses video_ioctl2 for parsing and to add debug support
+ */
+
+
+#include <linux/module.h>
+#include <linux/init.h>
+#include <linux/ioport.h>
+#include <linux/delay.h>
+#include <linux/mutex.h>
+#include <linux/pci.h>
+#include <linux/videodev2.h>
+#include <linux/io.h>
+#include <linux/slab.h>
+#include <media/drv-intf/tea575x.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-ioctl.h>
+#include <media/v4l2-fh.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-event.h>
+
+MODULE_AUTHOR("Dimitromanolakis Apostolos, apdim@grecian.net");
+MODULE_DESCRIPTION("Radio driver for the Guillemot Maxi Radio FM2000.");
+MODULE_LICENSE("GPL");
+MODULE_VERSION("1.0.0");
+
+static int radio_nr = -1;
+module_param(radio_nr, int, 0644);
+MODULE_PARM_DESC(radio_nr, "Radio device number");
+
+/* TEA5757 pin mappings */
+static const int clk = 1, data = 2, wren = 4, mo_st = 8, power = 16;
+
+static atomic_t maxiradio_instance = ATOMIC_INIT(0);
+
+#define PCI_VENDOR_ID_GUILLEMOT 0x5046
+#define PCI_DEVICE_ID_GUILLEMOT_MAXIRADIO 0x1001
+
+struct maxiradio
+{
+ struct snd_tea575x tea;
+ struct v4l2_device v4l2_dev;
+ struct pci_dev *pdev;
+
+ u16 io; /* base of radio io */
+};
+
+static inline struct maxiradio *to_maxiradio(struct v4l2_device *v4l2_dev)
+{
+ return container_of(v4l2_dev, struct maxiradio, v4l2_dev);
+}
+
+static void maxiradio_tea575x_set_pins(struct snd_tea575x *tea, u8 pins)
+{
+ struct maxiradio *dev = tea->private_data;
+ u8 bits = 0;
+
+ bits |= (pins & TEA575X_DATA) ? data : 0;
+ bits |= (pins & TEA575X_CLK) ? clk : 0;
+ bits |= (pins & TEA575X_WREN) ? wren : 0;
+ bits |= power;
+
+ outb(bits, dev->io);
+}
+
+/* Note: this card cannot read out the data of the shift registers,
+ only the mono/stereo pin works. */
+static u8 maxiradio_tea575x_get_pins(struct snd_tea575x *tea)
+{
+ struct maxiradio *dev = tea->private_data;
+ u8 bits = inb(dev->io);
+
+ return ((bits & data) ? TEA575X_DATA : 0) |
+ ((bits & mo_st) ? TEA575X_MOST : 0);
+}
+
+static void maxiradio_tea575x_set_direction(struct snd_tea575x *tea, bool output)
+{
+}
+
+static const struct snd_tea575x_ops maxiradio_tea_ops = {
+ .set_pins = maxiradio_tea575x_set_pins,
+ .get_pins = maxiradio_tea575x_get_pins,
+ .set_direction = maxiradio_tea575x_set_direction,
+};
+
+static int maxiradio_probe(struct pci_dev *pdev,
+ const struct pci_device_id *ent)
+{
+ struct maxiradio *dev;
+ struct v4l2_device *v4l2_dev;
+ int retval = -ENOMEM;
+
+ dev = kzalloc_obj(*dev);
+ if (dev == NULL) {
+ dev_err(&pdev->dev, "not enough memory\n");
+ return -ENOMEM;
+ }
+
+ v4l2_dev = &dev->v4l2_dev;
+ v4l2_device_set_name(v4l2_dev, "maxiradio", &maxiradio_instance);
+
+ retval = v4l2_device_register(&pdev->dev, v4l2_dev);
+ if (retval < 0) {
+ v4l2_err(v4l2_dev, "Could not register v4l2_device\n");
+ goto errfr;
+ }
+ dev->tea.private_data = dev;
+ dev->tea.ops = &maxiradio_tea_ops;
+ /* The data pin cannot be read. This may be a hardware limitation, or
+ we just don't know how to read it. */
+ dev->tea.cannot_read_data = true;
+ dev->tea.v4l2_dev = v4l2_dev;
+ dev->tea.radio_nr = radio_nr;
+ strscpy(dev->tea.card, "Maxi Radio FM2000", sizeof(dev->tea.card));
+
+ retval = -ENODEV;
+
+ if (!request_region(pci_resource_start(pdev, 0),
+ pci_resource_len(pdev, 0), v4l2_dev->name)) {
+ dev_err(&pdev->dev, "can't reserve I/O ports\n");
+ goto err_hdl;
+ }
+
+ if (pci_enable_device(pdev))
+ goto err_out_free_region;
+
+ dev->io = pci_resource_start(pdev, 0);
+ if (snd_tea575x_init(&dev->tea, THIS_MODULE)) {
+ printk(KERN_ERR "radio-maxiradio: Unable to detect TEA575x tuner\n");
+ goto err_out_free_region;
+ }
+ return 0;
+
+err_out_free_region:
+ release_region(pci_resource_start(pdev, 0), pci_resource_len(pdev, 0));
+err_hdl:
+ v4l2_device_unregister(v4l2_dev);
+errfr:
+ kfree(dev);
+ return retval;
+}
+
+static void maxiradio_remove(struct pci_dev *pdev)
+{
+ struct v4l2_device *v4l2_dev = pci_get_drvdata(pdev);
+ struct maxiradio *dev = to_maxiradio(v4l2_dev);
+
+ snd_tea575x_exit(&dev->tea);
+ /* Turn off power */
+ outb(0, dev->io);
+ v4l2_device_unregister(v4l2_dev);
+ release_region(pci_resource_start(pdev, 0), pci_resource_len(pdev, 0));
+ kfree(dev);
+}
+
+static const struct pci_device_id maxiradio_pci_tbl[] = {
+ { PCI_VENDOR_ID_GUILLEMOT, PCI_DEVICE_ID_GUILLEMOT_MAXIRADIO,
+ PCI_ANY_ID, PCI_ANY_ID, },
+ { 0 }
+};
+
+MODULE_DEVICE_TABLE(pci, maxiradio_pci_tbl);
+
+static struct pci_driver maxiradio_driver = {
+ .name = "radio-maxiradio",
+ .id_table = maxiradio_pci_tbl,
+ .probe = maxiradio_probe,
+ .remove = maxiradio_remove,
+};
+
+module_pci_driver(maxiradio_driver);
diff --git a/drivers/media/radio/radio-miropcm20.c b/drivers/media/radio/radio-miropcm20.c
new file mode 100644
index 000000000..67712ab3d
--- /dev/null
+++ b/drivers/media/radio/radio-miropcm20.c
@@ -0,0 +1,513 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Miro PCM20 radio driver for Linux radio support
+ * (c) 1998 Ruurd Reitsma <R.A.Reitsma@wbmt.tudelft.nl>
+ * Thanks to Norberto Pellici for the ACI device interface specification
+ * The API part is based on the radiotrack driver by M. Kirkwood
+ * This driver relies on the aci mixer provided by the snd-miro
+ * ALSA driver.
+ * Look there for further info...
+ *
+ * From the original miro RDS sources:
+ *
+ * (c) 2001 Robert Siemer <Robert.Siemer@gmx.de>
+ *
+ * Many thanks to Fred Seidel <seidel@metabox.de>, the
+ * designer of the RDS decoder hardware. With his help
+ * I was able to code this driver.
+ * Thanks also to Norberto Pellicci, Dominic Mounteney
+ * <DMounteney@pinnaclesys.com> and www.teleauskunft.de
+ * for good hints on finding Fred. It was somewhat hard
+ * to locate him here in Germany... [:
+ *
+ * This code has been reintroduced and converted to use
+ * the new V4L2 RDS API by:
+ *
+ * Hans Verkuil <hverkuil@kernel.org>
+ */
+
+#include <linux/module.h>
+#include <linux/init.h>
+#include <linux/io.h>
+#include <linux/delay.h>
+#include <linux/videodev2.h>
+#include <linux/kthread.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-ioctl.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-fh.h>
+#include <media/v4l2-event.h>
+#include <sound/aci.h>
+
+#define RDS_DATASHIFT 2 /* Bit 2 */
+#define RDS_DATAMASK (1 << RDS_DATASHIFT)
+#define RDS_BUSYMASK 0x10 /* Bit 4 */
+#define RDS_CLOCKMASK 0x08 /* Bit 3 */
+#define RDS_DATA(x) (((x) >> RDS_DATASHIFT) & 1)
+
+#define RDS_STATUS 0x01
+#define RDS_STATIONNAME 0x02
+#define RDS_TEXT 0x03
+#define RDS_ALTFREQ 0x04
+#define RDS_TIMEDATE 0x05
+#define RDS_PI_CODE 0x06
+#define RDS_PTYTATP 0x07
+#define RDS_RESET 0x08
+#define RDS_RXVALUE 0x09
+
+static int radio_nr = -1;
+module_param(radio_nr, int, 0);
+MODULE_PARM_DESC(radio_nr, "Set radio device number (/dev/radioX). Default: -1 (autodetect)");
+
+struct pcm20 {
+ struct v4l2_device v4l2_dev;
+ struct video_device vdev;
+ struct v4l2_ctrl_handler ctrl_handler;
+ struct v4l2_ctrl *rds_pty;
+ struct v4l2_ctrl *rds_ps_name;
+ struct v4l2_ctrl *rds_radio_test;
+ struct v4l2_ctrl *rds_ta;
+ struct v4l2_ctrl *rds_tp;
+ struct v4l2_ctrl *rds_ms;
+ /* thread for periodic RDS status checking */
+ struct task_struct *kthread;
+ unsigned long freq;
+ u32 audmode;
+ struct snd_miro_aci *aci;
+ struct mutex lock;
+};
+
+static struct pcm20 pcm20_card = {
+ .freq = 87 * 16000,
+ .audmode = V4L2_TUNER_MODE_STEREO,
+};
+
+
+static int rds_waitread(struct snd_miro_aci *aci)
+{
+ u8 byte;
+ int i = 2000;
+
+ do {
+ byte = inb(aci->aci_port + ACI_REG_RDS);
+ i--;
+ } while ((byte & RDS_BUSYMASK) && i);
+
+ /*
+ * It's magic, but without this the data that you read later on
+ * is unreliable and full of bit errors. With this 1 usec delay
+ * everything is fine.
+ */
+ udelay(1);
+ return i ? byte : -1;
+}
+
+static int rds_rawwrite(struct snd_miro_aci *aci, u8 byte)
+{
+ if (rds_waitread(aci) >= 0) {
+ outb(byte, aci->aci_port + ACI_REG_RDS);
+ return 0;
+ }
+ return -1;
+}
+
+static int rds_write(struct snd_miro_aci *aci, u8 byte)
+{
+ u8 sendbuffer[8];
+ int i;
+
+ for (i = 7; i >= 0; i--)
+ sendbuffer[7 - i] = (byte & (1 << i)) ? RDS_DATAMASK : 0;
+ sendbuffer[0] |= RDS_CLOCKMASK;
+
+ for (i = 0; i < 8; i++)
+ rds_rawwrite(aci, sendbuffer[i]);
+ return 0;
+}
+
+static int rds_readcycle_nowait(struct snd_miro_aci *aci)
+{
+ outb(0, aci->aci_port + ACI_REG_RDS);
+ return rds_waitread(aci);
+}
+
+static int rds_readcycle(struct snd_miro_aci *aci)
+{
+ if (rds_rawwrite(aci, 0) < 0)
+ return -1;
+ return rds_waitread(aci);
+}
+
+static int rds_ack(struct snd_miro_aci *aci)
+{
+ int i = rds_readcycle(aci);
+
+ if (i < 0)
+ return -1;
+ if (i & RDS_DATAMASK)
+ return 0; /* ACK */
+ return 1; /* NACK */
+}
+
+static int rds_cmd(struct snd_miro_aci *aci, u8 cmd, u8 databuffer[], u8 datasize)
+{
+ int i, j;
+
+ rds_write(aci, cmd);
+
+ /* RDS_RESET doesn't need further processing */
+ if (cmd == RDS_RESET)
+ return 0;
+ if (rds_ack(aci))
+ return -EIO;
+ if (datasize == 0)
+ return 0;
+
+ /* to be able to use rds_readcycle_nowait()
+ I have to waitread() here */
+ if (rds_waitread(aci) < 0)
+ return -1;
+
+ memset(databuffer, 0, datasize);
+
+ for (i = 0; i < 8 * datasize; i++) {
+ j = rds_readcycle_nowait(aci);
+ if (j < 0)
+ return -EIO;
+ databuffer[i / 8] |= RDS_DATA(j) << (7 - (i % 8));
+ }
+ return 0;
+}
+
+static int pcm20_setfreq(struct pcm20 *dev, unsigned long freq)
+{
+ unsigned char freql;
+ unsigned char freqh;
+ struct snd_miro_aci *aci = dev->aci;
+
+ freq /= 160;
+ if (!(aci->aci_version == 0x07 || aci->aci_version >= 0xb0))
+ freq /= 10; /* I don't know exactly which version
+ * needs this hack */
+ freql = freq & 0xff;
+ freqh = freq >> 8;
+
+ rds_cmd(aci, RDS_RESET, NULL, 0);
+ return snd_aci_cmd(aci, ACI_WRITE_TUNE, freql, freqh);
+}
+
+static int vidioc_querycap(struct file *file, void *priv,
+ struct v4l2_capability *v)
+{
+ strscpy(v->driver, "Miro PCM20", sizeof(v->driver));
+ strscpy(v->card, "Miro PCM20", sizeof(v->card));
+ strscpy(v->bus_info, "ISA:radio-miropcm20", sizeof(v->bus_info));
+ return 0;
+}
+
+static bool sanitize(char *p, int size)
+{
+ int i;
+ bool ret = true;
+
+ for (i = 0; i < size; i++) {
+ if (p[i] < 32) {
+ p[i] = ' ';
+ ret = false;
+ }
+ }
+ return ret;
+}
+
+static int vidioc_g_tuner(struct file *file, void *priv,
+ struct v4l2_tuner *v)
+{
+ struct pcm20 *dev = video_drvdata(file);
+ int res;
+ u8 buf;
+
+ if (v->index)
+ return -EINVAL;
+ strscpy(v->name, "FM", sizeof(v->name));
+ v->type = V4L2_TUNER_RADIO;
+ v->rangelow = 87*16000;
+ v->rangehigh = 108*16000;
+ res = snd_aci_cmd(dev->aci, ACI_READ_TUNERSTATION, -1, -1);
+ v->signal = (res & 0x80) ? 0 : 0xffff;
+ /* Note: stereo detection does not work if the audio is muted,
+ it will default to mono in that case. */
+ res = snd_aci_cmd(dev->aci, ACI_READ_TUNERSTEREO, -1, -1);
+ v->rxsubchans = (res & 0x40) ? V4L2_TUNER_SUB_MONO :
+ V4L2_TUNER_SUB_STEREO;
+ v->capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO |
+ V4L2_TUNER_CAP_RDS | V4L2_TUNER_CAP_RDS_CONTROLS;
+ v->audmode = dev->audmode;
+ res = rds_cmd(dev->aci, RDS_RXVALUE, &buf, 1);
+ if (res >= 0 && buf)
+ v->rxsubchans |= V4L2_TUNER_SUB_RDS;
+ return 0;
+}
+
+static int vidioc_s_tuner(struct file *file, void *priv,
+ const struct v4l2_tuner *v)
+{
+ struct pcm20 *dev = video_drvdata(file);
+
+ if (v->index)
+ return -EINVAL;
+ if (v->audmode > V4L2_TUNER_MODE_STEREO)
+ dev->audmode = V4L2_TUNER_MODE_STEREO;
+ else
+ dev->audmode = v->audmode;
+ snd_aci_cmd(dev->aci, ACI_SET_TUNERMONO,
+ dev->audmode == V4L2_TUNER_MODE_MONO, -1);
+ return 0;
+}
+
+static int vidioc_g_frequency(struct file *file, void *priv,
+ struct v4l2_frequency *f)
+{
+ struct pcm20 *dev = video_drvdata(file);
+
+ if (f->tuner != 0)
+ return -EINVAL;
+
+ f->type = V4L2_TUNER_RADIO;
+ f->frequency = dev->freq;
+ return 0;
+}
+
+
+static int vidioc_s_frequency(struct file *file, void *priv,
+ const struct v4l2_frequency *f)
+{
+ struct pcm20 *dev = video_drvdata(file);
+
+ if (f->tuner != 0 || f->type != V4L2_TUNER_RADIO)
+ return -EINVAL;
+
+ dev->freq = clamp_t(u32, f->frequency, 87 * 16000U, 108 * 16000U);
+ pcm20_setfreq(dev, dev->freq);
+ return 0;
+}
+
+static int pcm20_s_ctrl(struct v4l2_ctrl *ctrl)
+{
+ struct pcm20 *dev = container_of(ctrl->handler, struct pcm20, ctrl_handler);
+
+ switch (ctrl->id) {
+ case V4L2_CID_AUDIO_MUTE:
+ snd_aci_cmd(dev->aci, ACI_SET_TUNERMUTE, ctrl->val, -1);
+ return 0;
+ }
+ return -EINVAL;
+}
+
+static int pcm20_thread(void *data)
+{
+ struct pcm20 *dev = data;
+ const unsigned no_rds_start_counter = 5;
+ const unsigned sleep_msecs = 2000;
+ unsigned no_rds_counter = no_rds_start_counter;
+
+ for (;;) {
+ char text_buffer[66];
+ u8 buf;
+ int res;
+
+ msleep_interruptible(sleep_msecs);
+
+ if (kthread_should_stop())
+ break;
+
+ res = rds_cmd(dev->aci, RDS_RXVALUE, &buf, 1);
+ if (res)
+ continue;
+ if (buf == 0) {
+ if (no_rds_counter == 0)
+ continue;
+ no_rds_counter--;
+ if (no_rds_counter)
+ continue;
+
+ /*
+ * No RDS seen for no_rds_start_counter * sleep_msecs
+ * milliseconds, clear all RDS controls to their
+ * default values.
+ */
+ v4l2_ctrl_s_ctrl_string(dev->rds_ps_name, "");
+ v4l2_ctrl_s_ctrl(dev->rds_ms, 1);
+ v4l2_ctrl_s_ctrl(dev->rds_ta, 0);
+ v4l2_ctrl_s_ctrl(dev->rds_tp, 0);
+ v4l2_ctrl_s_ctrl(dev->rds_pty, 0);
+ v4l2_ctrl_s_ctrl_string(dev->rds_radio_test, "");
+ continue;
+ }
+ no_rds_counter = no_rds_start_counter;
+
+ res = rds_cmd(dev->aci, RDS_STATUS, &buf, 1);
+ if (res)
+ continue;
+ if ((buf >> 3) & 1) {
+ res = rds_cmd(dev->aci, RDS_STATIONNAME, text_buffer, 8);
+ text_buffer[8] = 0;
+ if (!res && sanitize(text_buffer, 8))
+ v4l2_ctrl_s_ctrl_string(dev->rds_ps_name, text_buffer);
+ }
+ if ((buf >> 6) & 1) {
+ u8 pty;
+
+ res = rds_cmd(dev->aci, RDS_PTYTATP, &pty, 1);
+ if (!res) {
+ v4l2_ctrl_s_ctrl(dev->rds_ms, !!(pty & 0x01));
+ v4l2_ctrl_s_ctrl(dev->rds_ta, !!(pty & 0x02));
+ v4l2_ctrl_s_ctrl(dev->rds_tp, !!(pty & 0x80));
+ v4l2_ctrl_s_ctrl(dev->rds_pty, (pty >> 2) & 0x1f);
+ }
+ }
+ if ((buf >> 4) & 1) {
+ res = rds_cmd(dev->aci, RDS_TEXT, text_buffer, 65);
+ text_buffer[65] = 0;
+ if (!res && sanitize(text_buffer + 1, 64))
+ v4l2_ctrl_s_ctrl_string(dev->rds_radio_test, text_buffer + 1);
+ }
+ }
+ return 0;
+}
+
+static int pcm20_open(struct file *file)
+{
+ struct pcm20 *dev = video_drvdata(file);
+ int res = v4l2_fh_open(file);
+
+ if (!res && v4l2_fh_is_singular_file(file) &&
+ IS_ERR_OR_NULL(dev->kthread)) {
+ dev->kthread = kthread_run(pcm20_thread, dev, "%s",
+ dev->v4l2_dev.name);
+ if (IS_ERR(dev->kthread)) {
+ v4l2_err(&dev->v4l2_dev, "kernel_thread() failed\n");
+ v4l2_fh_release(file);
+ return PTR_ERR(dev->kthread);
+ }
+ }
+ return res;
+}
+
+static int pcm20_release(struct file *file)
+{
+ struct pcm20 *dev = video_drvdata(file);
+
+ if (v4l2_fh_is_singular_file(file) && !IS_ERR_OR_NULL(dev->kthread)) {
+ kthread_stop(dev->kthread);
+ dev->kthread = NULL;
+ }
+ return v4l2_fh_release(file);
+}
+
+static const struct v4l2_file_operations pcm20_fops = {
+ .owner = THIS_MODULE,
+ .open = pcm20_open,
+ .poll = v4l2_ctrl_poll,
+ .release = pcm20_release,
+ .unlocked_ioctl = video_ioctl2,
+};
+
+static const struct v4l2_ioctl_ops pcm20_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 const struct v4l2_ctrl_ops pcm20_ctrl_ops = {
+ .s_ctrl = pcm20_s_ctrl,
+};
+
+static int __init pcm20_init(void)
+{
+ struct pcm20 *dev = &pcm20_card;
+ struct v4l2_device *v4l2_dev = &dev->v4l2_dev;
+ struct v4l2_ctrl_handler *hdl;
+ int res;
+
+ dev->aci = snd_aci_get_aci();
+ if (dev->aci == NULL) {
+ v4l2_err(v4l2_dev,
+ "you must load the snd-miro driver first!\n");
+ return -ENODEV;
+ }
+ strscpy(v4l2_dev->name, "radio-miropcm20", sizeof(v4l2_dev->name));
+ mutex_init(&dev->lock);
+
+ res = v4l2_device_register(NULL, v4l2_dev);
+ if (res < 0) {
+ v4l2_err(v4l2_dev, "could not register v4l2_device\n");
+ return -EINVAL;
+ }
+
+ hdl = &dev->ctrl_handler;
+ v4l2_ctrl_handler_init(hdl, 7);
+ v4l2_ctrl_new_std(hdl, &pcm20_ctrl_ops,
+ V4L2_CID_AUDIO_MUTE, 0, 1, 1, 1);
+ dev->rds_pty = v4l2_ctrl_new_std(hdl, NULL,
+ V4L2_CID_RDS_RX_PTY, 0, 0x1f, 1, 0);
+ dev->rds_ps_name = v4l2_ctrl_new_std(hdl, NULL,
+ V4L2_CID_RDS_RX_PS_NAME, 0, 8, 8, 0);
+ dev->rds_radio_test = v4l2_ctrl_new_std(hdl, NULL,
+ V4L2_CID_RDS_RX_RADIO_TEXT, 0, 64, 64, 0);
+ dev->rds_ta = v4l2_ctrl_new_std(hdl, NULL,
+ V4L2_CID_RDS_RX_TRAFFIC_ANNOUNCEMENT, 0, 1, 1, 0);
+ dev->rds_tp = v4l2_ctrl_new_std(hdl, NULL,
+ V4L2_CID_RDS_RX_TRAFFIC_PROGRAM, 0, 1, 1, 0);
+ dev->rds_ms = v4l2_ctrl_new_std(hdl, NULL,
+ V4L2_CID_RDS_RX_MUSIC_SPEECH, 0, 1, 1, 1);
+ v4l2_dev->ctrl_handler = hdl;
+ if (hdl->error) {
+ res = hdl->error;
+ v4l2_err(v4l2_dev, "Could not register control\n");
+ goto err_hdl;
+ }
+ strscpy(dev->vdev.name, v4l2_dev->name, sizeof(dev->vdev.name));
+ dev->vdev.v4l2_dev = v4l2_dev;
+ dev->vdev.fops = &pcm20_fops;
+ dev->vdev.ioctl_ops = &pcm20_ioctl_ops;
+ dev->vdev.release = video_device_release_empty;
+ dev->vdev.lock = &dev->lock;
+ dev->vdev.device_caps = V4L2_CAP_TUNER | V4L2_CAP_RADIO |
+ V4L2_CAP_RDS_CAPTURE;
+ video_set_drvdata(&dev->vdev, dev);
+ snd_aci_cmd(dev->aci, ACI_SET_TUNERMONO,
+ dev->audmode == V4L2_TUNER_MODE_MONO, -1);
+ pcm20_setfreq(dev, dev->freq);
+
+ if (video_register_device(&dev->vdev, VFL_TYPE_RADIO, radio_nr) < 0)
+ goto err_hdl;
+
+ v4l2_info(v4l2_dev, "Mirosound PCM20 Radio tuner\n");
+ return 0;
+err_hdl:
+ v4l2_ctrl_handler_free(hdl);
+ v4l2_device_unregister(v4l2_dev);
+ return -EINVAL;
+}
+
+MODULE_AUTHOR("Ruurd Reitsma, Krzysztof Helt");
+MODULE_DESCRIPTION("A driver for the Miro PCM20 radio card.");
+MODULE_LICENSE("GPL");
+
+static void __exit pcm20_cleanup(void)
+{
+ struct pcm20 *dev = &pcm20_card;
+
+ video_unregister_device(&dev->vdev);
+ snd_aci_cmd(dev->aci, ACI_SET_TUNERMUTE, 1, -1);
+ v4l2_ctrl_handler_free(&dev->ctrl_handler);
+ v4l2_device_unregister(&dev->v4l2_dev);
+}
+
+module_init(pcm20_init);
+module_exit(pcm20_cleanup);
diff --git a/drivers/media/radio/radio-mr800.c b/drivers/media/radio/radio-mr800.c
new file mode 100644
index 000000000..d5a6053e1
--- /dev/null
+++ b/drivers/media/radio/radio-mr800.c
@@ -0,0 +1,599 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A driver for the AverMedia MR 800 USB FM radio. This device plugs
+ * into both the USB and an analog audio input, so this thing
+ * only deals with initialization and frequency setting, the
+ * audio data has to be handled by a sound driver.
+ *
+ * Copyright (c) 2008 Alexey Klimov <klimov.linux@gmail.com>
+ */
+
+/*
+ * Big thanks to authors and contributors of dsbr100.c and radio-si470x.c
+ *
+ * When work was looked pretty good, i discover this:
+ * http://av-usbradio.sourceforge.net/index.php
+ * http://sourceforge.net/projects/av-usbradio/
+ * Latest release of theirs project was in 2005.
+ * Probably, this driver could be improved through using their
+ * achievements (specifications given).
+ * Also, Faidon Liambotis <paravoid@debian.org> wrote nice driver for this radio
+ * in 2007. He allowed to use his driver to improve current mr800 radio driver.
+ * http://www.spinics.net/lists/linux-usb-devel/msg10109.html
+ *
+ * Version 0.01: First working version.
+ * It's required to blacklist AverMedia USB Radio
+ * in usbhid/hid-quirks.c
+ * Version 0.10: A lot of cleanups and fixes: unpluging the device,
+ * few mutex locks were added, codinstyle issues, etc.
+ * Added stereo support. Thanks to
+ * Douglas Schilling Landgraf <dougsland@gmail.com> and
+ * David Ellingsworth <david@identd.dyndns.org>
+ * for discussion, help and support.
+ * Version 0.11: Converted to v4l2_device.
+ *
+ * Many things to do:
+ * - Correct power management of device (suspend & resume)
+ * - Add code for scanning and smooth tuning
+ * - Add code for sensitivity value
+ * - Correct mistakes
+ * - In Japan another FREQ_MIN and FREQ_MAX
+ */
+
+/* kernel includes */
+#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>
+
+/* driver and module definitions */
+#define DRIVER_AUTHOR "Alexey Klimov <klimov.linux@gmail.com>"
+#define DRIVER_DESC "AverMedia MR 800 USB FM radio driver"
+#define DRIVER_VERSION "0.1.2"
+
+MODULE_AUTHOR(DRIVER_AUTHOR);
+MODULE_DESCRIPTION(DRIVER_DESC);
+MODULE_LICENSE("GPL");
+MODULE_VERSION(DRIVER_VERSION);
+
+#define USB_AMRADIO_VENDOR 0x07ca
+#define USB_AMRADIO_PRODUCT 0xb800
+
+/* dev_warn macro with driver name */
+#define MR800_DRIVER_NAME "radio-mr800"
+#define amradio_dev_warn(dev, fmt, arg...) \
+ dev_warn(dev, MR800_DRIVER_NAME " - " fmt, ##arg)
+
+#define amradio_dev_err(dev, fmt, arg...) \
+ dev_err(dev, MR800_DRIVER_NAME " - " fmt, ##arg)
+
+/* Probably USB_TIMEOUT should be modified in module parameter */
+#define BUFFER_LENGTH 8
+#define USB_TIMEOUT 500
+
+/* 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
+
+/*
+ * Commands that device should understand
+ * List isn't full and will be updated with implementation of new functions
+ */
+#define AMRADIO_SET_FREQ 0xa4
+#define AMRADIO_GET_READY_FLAG 0xa5
+#define AMRADIO_GET_SIGNAL 0xa7
+#define AMRADIO_GET_FREQ 0xa8
+#define AMRADIO_SET_SEARCH_UP 0xa9
+#define AMRADIO_SET_SEARCH_DOWN 0xaa
+#define AMRADIO_SET_MUTE 0xab
+#define AMRADIO_SET_RIGHT_MUTE 0xac
+#define AMRADIO_SET_LEFT_MUTE 0xad
+#define AMRADIO_SET_MONO 0xae
+#define AMRADIO_SET_SEARCH_LVL 0xb0
+#define AMRADIO_STOP_SEARCH 0xb1
+
+/* Comfortable defines for amradio_set_stereo */
+#define WANT_STEREO 0x00
+#define WANT_MONO 0x01
+
+/* module parameter */
+static int radio_nr = -1;
+module_param(radio_nr, int, 0);
+MODULE_PARM_DESC(radio_nr, "Radio Nr");
+
+/* Data for one (physical) device */
+struct amradio_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;
+ int stereo;
+ int muted;
+};
+
+static inline struct amradio_device *to_amradio_dev(struct v4l2_device *v4l2_dev)
+{
+ return container_of(v4l2_dev, struct amradio_device, v4l2_dev);
+}
+
+static int amradio_send_cmd(struct amradio_device *radio, u8 cmd, u8 arg,
+ u8 *extra, u8 extralen, bool reply)
+{
+ int retval;
+ int size;
+
+ radio->buffer[0] = 0x00;
+ radio->buffer[1] = 0x55;
+ radio->buffer[2] = 0xaa;
+ radio->buffer[3] = extralen;
+ radio->buffer[4] = cmd;
+ radio->buffer[5] = arg;
+ radio->buffer[6] = 0x00;
+ radio->buffer[7] = extra || reply ? 8 : 0;
+
+ retval = usb_bulk_msg(radio->usbdev, usb_sndintpipe(radio->usbdev, 2),
+ radio->buffer, BUFFER_LENGTH, &size, USB_TIMEOUT);
+
+ if (retval < 0 || size != BUFFER_LENGTH) {
+ if (video_is_registered(&radio->vdev))
+ amradio_dev_warn(&radio->vdev.dev,
+ "cmd %02x failed\n", cmd);
+ return retval ? retval : -EIO;
+ }
+ if (!extra && !reply)
+ return 0;
+
+ if (extra) {
+ memcpy(radio->buffer, extra, extralen);
+ memset(radio->buffer + extralen, 0, 8 - extralen);
+ retval = usb_bulk_msg(radio->usbdev, usb_sndintpipe(radio->usbdev, 2),
+ radio->buffer, BUFFER_LENGTH, &size, USB_TIMEOUT);
+ } else {
+ memset(radio->buffer, 0, 8);
+ retval = usb_bulk_msg(radio->usbdev, usb_rcvbulkpipe(radio->usbdev, 0x81),
+ radio->buffer, BUFFER_LENGTH, &size, USB_TIMEOUT);
+ }
+ if (retval == 0 && size == BUFFER_LENGTH)
+ return 0;
+ if (video_is_registered(&radio->vdev) && cmd != AMRADIO_GET_READY_FLAG)
+ amradio_dev_warn(&radio->vdev.dev, "follow-up to cmd %02x failed\n", cmd);
+ return retval ? retval : -EIO;
+}
+
+/* switch on/off the radio. Send 8 bytes to device */
+static int amradio_set_mute(struct amradio_device *radio, bool mute)
+{
+ int ret = amradio_send_cmd(radio,
+ AMRADIO_SET_MUTE, mute, NULL, 0, false);
+
+ if (!ret)
+ radio->muted = mute;
+ return ret;
+}
+
+/* set a frequency, freq is defined by v4l's TUNER_LOW, i.e. 1/16th kHz */
+static int amradio_set_freq(struct amradio_device *radio, int freq)
+{
+ unsigned short freq_send;
+ u8 buf[3];
+ int retval;
+
+ /* we need to be sure that frequency isn't out of range */
+ freq = clamp_t(unsigned, freq, FREQ_MIN * FREQ_MUL, FREQ_MAX * FREQ_MUL);
+ freq_send = 0x10 + (freq >> 3) / 25;
+
+ /* frequency is calculated from freq_send and placed in first 2 bytes */
+ buf[0] = (freq_send >> 8) & 0xff;
+ buf[1] = freq_send & 0xff;
+ buf[2] = 0x01;
+
+ retval = amradio_send_cmd(radio, AMRADIO_SET_FREQ, 0, buf, 3, false);
+ if (retval)
+ return retval;
+ radio->curfreq = freq;
+ msleep(40);
+ return 0;
+}
+
+static int amradio_set_stereo(struct amradio_device *radio, bool stereo)
+{
+ int ret = amradio_send_cmd(radio,
+ AMRADIO_SET_MONO, !stereo, NULL, 0, false);
+
+ if (!ret)
+ radio->stereo = stereo;
+ return ret;
+}
+
+static int amradio_get_stat(struct amradio_device *radio, bool *is_stereo, u32 *signal)
+{
+ int ret = amradio_send_cmd(radio,
+ AMRADIO_GET_SIGNAL, 0, NULL, 0, true);
+
+ if (ret)
+ return ret;
+ *is_stereo = radio->buffer[2] >> 7;
+ *signal = (radio->buffer[3] & 0xf0) << 8;
+ return 0;
+}
+
+/* Handle unplugging the device.
+ * We call video_unregister_device in any case.
+ * The last function called in this procedure is
+ * usb_amradio_device_release.
+ */
+static void usb_amradio_disconnect(struct usb_interface *intf)
+{
+ struct amradio_device *radio = to_amradio_dev(usb_get_intfdata(intf));
+
+ mutex_lock(&radio->lock);
+ video_unregister_device(&radio->vdev);
+ amradio_set_mute(radio, true);
+ 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 amradio_device *radio = video_drvdata(file);
+
+ strscpy(v->driver, "radio-mr800", sizeof(v->driver));
+ strscpy(v->card, "AverMedia MR 800 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 amradio_device *radio = video_drvdata(file);
+ bool is_stereo = false;
+ int retval;
+
+ if (v->index > 0)
+ return -EINVAL;
+
+ v->signal = 0;
+ retval = amradio_get_stat(radio, &is_stereo, &v->signal);
+ if (retval)
+ return retval;
+
+ 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 |
+ V4L2_TUNER_CAP_HWSEEK_WRAP;
+ 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 amradio_device *radio = video_drvdata(file);
+
+ if (v->index > 0)
+ return -EINVAL;
+
+ /* mono/stereo selector */
+ switch (v->audmode) {
+ case V4L2_TUNER_MODE_MONO:
+ return amradio_set_stereo(radio, WANT_MONO);
+ default:
+ return amradio_set_stereo(radio, 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 amradio_device *radio = video_drvdata(file);
+
+ if (f->tuner != 0)
+ return -EINVAL;
+ return amradio_set_freq(radio, f->frequency);
+}
+
+/* vidioc_g_frequency - get tuner radio frequency */
+static int vidioc_g_frequency(struct file *file, void *priv,
+ struct v4l2_frequency *f)
+{
+ struct amradio_device *radio = video_drvdata(file);
+
+ if (f->tuner != 0 || f->type != V4L2_TUNER_RADIO)
+ return -EINVAL;
+ f->type = V4L2_TUNER_RADIO;
+ f->frequency = radio->curfreq;
+
+ return 0;
+}
+
+static int vidioc_s_hw_freq_seek(struct file *file, void *priv,
+ const struct v4l2_hw_freq_seek *seek)
+{
+ static u8 buf[8] = {
+ 0x3d, 0x32, 0x0f, 0x08, 0x3d, 0x32, 0x0f, 0x08
+ };
+ struct amradio_device *radio = video_drvdata(file);
+ unsigned long timeout;
+ int retval;
+
+ if (seek->tuner != 0 || !seek->wrap_around)
+ return -EINVAL;
+
+ if (file->f_flags & O_NONBLOCK)
+ return -EWOULDBLOCK;
+
+ retval = amradio_send_cmd(radio,
+ AMRADIO_SET_SEARCH_LVL, 0, buf, 8, false);
+ if (retval)
+ return retval;
+ amradio_set_freq(radio, radio->curfreq);
+ retval = amradio_send_cmd(radio,
+ seek->seek_upward ? AMRADIO_SET_SEARCH_UP : AMRADIO_SET_SEARCH_DOWN,
+ 0, NULL, 0, false);
+ if (retval)
+ return retval;
+ timeout = jiffies + msecs_to_jiffies(30000);
+ for (;;) {
+ if (time_after(jiffies, timeout)) {
+ retval = -ENODATA;
+ break;
+ }
+ if (schedule_timeout_interruptible(msecs_to_jiffies(10))) {
+ retval = -ERESTARTSYS;
+ break;
+ }
+ retval = amradio_send_cmd(radio, AMRADIO_GET_READY_FLAG,
+ 0, NULL, 0, true);
+ if (retval)
+ continue;
+ amradio_send_cmd(radio, AMRADIO_GET_FREQ, 0, NULL, 0, true);
+ if (radio->buffer[1] || radio->buffer[2]) {
+ /* To check: sometimes radio->curfreq is set to out of range value */
+ radio->curfreq = (radio->buffer[1] << 8) | radio->buffer[2];
+ radio->curfreq = (radio->curfreq - 0x10) * 200;
+ amradio_send_cmd(radio, AMRADIO_STOP_SEARCH,
+ 0, NULL, 0, false);
+ amradio_set_freq(radio, radio->curfreq);
+ retval = 0;
+ break;
+ }
+ }
+ amradio_send_cmd(radio, AMRADIO_STOP_SEARCH, 0, NULL, 0, false);
+ amradio_set_freq(radio, radio->curfreq);
+ return retval;
+}
+
+static int usb_amradio_s_ctrl(struct v4l2_ctrl *ctrl)
+{
+ struct amradio_device *radio =
+ container_of(ctrl->handler, struct amradio_device, hdl);
+
+ switch (ctrl->id) {
+ case V4L2_CID_AUDIO_MUTE:
+ return amradio_set_mute(radio, ctrl->val);
+ }
+
+ return -EINVAL;
+}
+
+static int usb_amradio_init(struct amradio_device *radio)
+{
+ int retval;
+
+ retval = amradio_set_mute(radio, true);
+ if (retval)
+ goto out_err;
+ retval = amradio_set_stereo(radio, true);
+ if (retval)
+ goto out_err;
+ retval = amradio_set_freq(radio, radio->curfreq);
+ if (retval)
+ goto out_err;
+ return 0;
+
+out_err:
+ amradio_dev_err(&radio->vdev.dev, "initialization failed\n");
+ return retval;
+}
+
+/* Suspend device - stop device. Need to be checked and fixed */
+static int usb_amradio_suspend(struct usb_interface *intf, pm_message_t message)
+{
+ struct amradio_device *radio = to_amradio_dev(usb_get_intfdata(intf));
+
+ mutex_lock(&radio->lock);
+ if (!radio->muted) {
+ amradio_set_mute(radio, true);
+ radio->muted = false;
+ }
+ mutex_unlock(&radio->lock);
+
+ dev_info(&intf->dev, "going into suspend..\n");
+ return 0;
+}
+
+/* Resume device - start device. Need to be checked and fixed */
+static int usb_amradio_resume(struct usb_interface *intf)
+{
+ struct amradio_device *radio = to_amradio_dev(usb_get_intfdata(intf));
+
+ mutex_lock(&radio->lock);
+ amradio_set_stereo(radio, radio->stereo);
+ amradio_set_freq(radio, radio->curfreq);
+
+ if (!radio->muted)
+ amradio_set_mute(radio, false);
+
+ mutex_unlock(&radio->lock);
+
+ dev_info(&intf->dev, "coming out of suspend..\n");
+ return 0;
+}
+
+static const struct v4l2_ctrl_ops usb_amradio_ctrl_ops = {
+ .s_ctrl = usb_amradio_s_ctrl,
+};
+
+/* File system interface */
+static const struct v4l2_file_operations usb_amradio_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_amradio_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_s_hw_freq_seek = vidioc_s_hw_freq_seek,
+ .vidioc_log_status = v4l2_ctrl_log_status,
+ .vidioc_subscribe_event = v4l2_ctrl_subscribe_event,
+ .vidioc_unsubscribe_event = v4l2_event_unsubscribe,
+};
+
+static void usb_amradio_release(struct v4l2_device *v4l2_dev)
+{
+ struct amradio_device *radio = to_amradio_dev(v4l2_dev);
+
+ /* free rest memory */
+ 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_amradio_probe(struct usb_interface *intf,
+ const struct usb_device_id *id)
+{
+ struct amradio_device *radio;
+ int retval;
+
+ radio = kzalloc_obj(struct amradio_device);
+
+ if (!radio) {
+ dev_err(&intf->dev, "kmalloc for amradio_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);
+ v4l2_ctrl_new_std(&radio->hdl, &usb_amradio_ctrl_ops,
+ V4L2_CID_AUDIO_MUTE, 0, 1, 1, 1);
+ 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_amradio_release;
+ strscpy(radio->vdev.name, radio->v4l2_dev.name,
+ sizeof(radio->vdev.name));
+ radio->vdev.v4l2_dev = &radio->v4l2_dev;
+ radio->vdev.fops = &usb_amradio_fops;
+ radio->vdev.ioctl_ops = &usb_amradio_ioctl_ops;
+ radio->vdev.release = video_device_release_empty;
+ radio->vdev.lock = &radio->lock;
+ radio->vdev.device_caps = V4L2_CAP_RADIO | V4L2_CAP_TUNER |
+ V4L2_CAP_HW_FREQ_SEEK;
+
+ radio->usbdev = interface_to_usbdev(intf);
+ radio->intf = intf;
+ usb_set_intfdata(intf, &radio->v4l2_dev);
+ radio->curfreq = 95.16 * FREQ_MUL;
+
+ video_set_drvdata(&radio->vdev, radio);
+ retval = usb_amradio_init(radio);
+ if (retval)
+ goto err_vdev;
+
+ 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_amradio_device_table[] = {
+ { USB_DEVICE_AND_INTERFACE_INFO(USB_AMRADIO_VENDOR, USB_AMRADIO_PRODUCT,
+ USB_CLASS_HID, 0, 0) },
+ { } /* Terminating entry */
+};
+
+MODULE_DEVICE_TABLE(usb, usb_amradio_device_table);
+
+/* USB subsystem interface */
+static struct usb_driver usb_amradio_driver = {
+ .name = MR800_DRIVER_NAME,
+ .probe = usb_amradio_probe,
+ .disconnect = usb_amradio_disconnect,
+ .suspend = usb_amradio_suspend,
+ .resume = usb_amradio_resume,
+ .reset_resume = usb_amradio_resume,
+ .id_table = usb_amradio_device_table,
+};
+
+module_usb_driver(usb_amradio_driver);
diff --git a/drivers/media/radio/radio-raremono.c b/drivers/media/radio/radio-raremono.c
new file mode 100644
index 000000000..6aa7c0978
--- /dev/null
+++ b/drivers/media/radio/radio-raremono.c
@@ -0,0 +1,389 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright 2013 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
+ */
+
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/init.h>
+#include <linux/slab.h>
+#include <linux/input.h>
+#include <linux/usb.h>
+#include <linux/hid.h>
+#include <linux/mutex.h>
+#include <linux/videodev2.h>
+#include <linux/unaligned.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-ioctl.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-event.h>
+
+/*
+ * 'Thanko's Raremono' is a Japanese si4734-based AM/FM/SW USB receiver:
+ *
+ * http://www.raremono.jp/product/484.html/
+ *
+ * The USB protocol has been reversed engineered using wireshark, initially
+ * by Dinesh Ram <dinesh.ram@cern.ch> and finished by Hans Verkuil
+ * <hverkuil@kernel.org>.
+ *
+ * Sadly the firmware used in this product hides lots of goodies since the
+ * si4734 has more features than are supported by the firmware. Oh well...
+ */
+
+/* driver and module definitions */
+MODULE_AUTHOR("Hans Verkuil <hverkuil@kernel.org>");
+MODULE_DESCRIPTION("Thanko's Raremono AM/FM/SW Receiver USB driver");
+MODULE_LICENSE("GPL v2");
+
+/*
+ * The Device announces itself as Cygnal Integrated Products, Inc.
+ *
+ * The vendor and product IDs (and in fact all other lsusb information as
+ * well) are identical to the si470x Silicon Labs USB FM Radio Reference
+ * Design board, even though this card has a si4734 device. Clearly the
+ * designer of this product never bothered to change the USB IDs.
+ */
+
+/* USB Device ID List */
+static const struct usb_device_id usb_raremono_device_table[] = {
+ {USB_DEVICE_AND_INTERFACE_INFO(0x10c4, 0x818a, USB_CLASS_HID, 0, 0) },
+ { } /* Terminating entry */
+};
+
+MODULE_DEVICE_TABLE(usb, usb_raremono_device_table);
+
+#define BUFFER_LENGTH 64
+
+/* Timeout is set to a high value, could probably be reduced. Need more tests */
+#define USB_TIMEOUT 10000
+
+/* Frequency limits in KHz */
+#define FM_FREQ_RANGE_LOW 64000
+#define FM_FREQ_RANGE_HIGH 108000
+
+#define AM_FREQ_RANGE_LOW 520
+#define AM_FREQ_RANGE_HIGH 1710
+
+#define SW_FREQ_RANGE_LOW 2300
+#define SW_FREQ_RANGE_HIGH 26100
+
+enum { BAND_FM, BAND_AM, BAND_SW };
+
+static const struct v4l2_frequency_band bands[] = {
+ /* Band FM */
+ {
+ .type = V4L2_TUNER_RADIO,
+ .index = 0,
+ .capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO |
+ V4L2_TUNER_CAP_FREQ_BANDS,
+ .rangelow = FM_FREQ_RANGE_LOW * 16,
+ .rangehigh = FM_FREQ_RANGE_HIGH * 16,
+ .modulation = V4L2_BAND_MODULATION_FM,
+ },
+ /* Band AM */
+ {
+ .type = V4L2_TUNER_RADIO,
+ .index = 1,
+ .capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_FREQ_BANDS,
+ .rangelow = AM_FREQ_RANGE_LOW * 16,
+ .rangehigh = AM_FREQ_RANGE_HIGH * 16,
+ .modulation = V4L2_BAND_MODULATION_AM,
+ },
+ /* Band SW */
+ {
+ .type = V4L2_TUNER_RADIO,
+ .index = 2,
+ .capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_FREQ_BANDS,
+ .rangelow = SW_FREQ_RANGE_LOW * 16,
+ .rangehigh = SW_FREQ_RANGE_HIGH * 16,
+ .modulation = V4L2_BAND_MODULATION_AM,
+ },
+};
+
+struct raremono_device {
+ struct usb_device *usbdev;
+ struct usb_interface *intf;
+ struct video_device vdev;
+ struct v4l2_device v4l2_dev;
+ struct mutex lock;
+
+ u8 *buffer;
+ u32 band;
+ unsigned curfreq;
+};
+
+static inline struct raremono_device *to_raremono_dev(struct v4l2_device *v4l2_dev)
+{
+ return container_of(v4l2_dev, struct raremono_device, v4l2_dev);
+}
+
+/* Set frequency. */
+static int raremono_cmd_main(struct raremono_device *radio, unsigned band, unsigned freq)
+{
+ unsigned band_offset;
+ int ret;
+
+ switch (band) {
+ case BAND_FM:
+ band_offset = 1;
+ freq /= 10;
+ break;
+ case BAND_AM:
+ band_offset = 0;
+ break;
+ default:
+ band_offset = 2;
+ break;
+ }
+ radio->buffer[0] = 0x04 + band_offset;
+ radio->buffer[1] = freq >> 8;
+ radio->buffer[2] = freq & 0xff;
+
+ ret = usb_control_msg(radio->usbdev, usb_sndctrlpipe(radio->usbdev, 0),
+ HID_REQ_SET_REPORT,
+ USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_OUT,
+ 0x0300 + radio->buffer[0], 2,
+ radio->buffer, 3, USB_TIMEOUT);
+
+ if (ret < 0) {
+ dev_warn(radio->v4l2_dev.dev, "%s failed (%d)\n", __func__, ret);
+ return ret;
+ }
+ radio->curfreq = (band == BAND_FM) ? freq * 10 : freq;
+ return 0;
+}
+
+/* Handle unplugging the device.
+ * We call video_unregister_device in any case.
+ * The last function called in this procedure is
+ * usb_raremono_device_release.
+ */
+static void usb_raremono_disconnect(struct usb_interface *intf)
+{
+ struct raremono_device *radio = to_raremono_dev(usb_get_intfdata(intf));
+
+ dev_info(&intf->dev, "Thanko's Raremono disconnected\n");
+
+ mutex_lock(&radio->lock);
+ usb_set_intfdata(intf, NULL);
+ video_unregister_device(&radio->vdev);
+ v4l2_device_disconnect(&radio->v4l2_dev);
+ mutex_unlock(&radio->lock);
+ v4l2_device_put(&radio->v4l2_dev);
+}
+
+/*
+ * Linux Video interface
+ */
+static int vidioc_querycap(struct file *file, void *priv,
+ struct v4l2_capability *v)
+{
+ struct raremono_device *radio = video_drvdata(file);
+
+ strscpy(v->driver, "radio-raremono", sizeof(v->driver));
+ strscpy(v->card, "Thanko's Raremono", sizeof(v->card));
+ usb_make_path(radio->usbdev, v->bus_info, sizeof(v->bus_info));
+ return 0;
+}
+
+static int vidioc_enum_freq_bands(struct file *file, void *priv,
+ struct v4l2_frequency_band *band)
+{
+ if (band->tuner != 0)
+ return -EINVAL;
+
+ if (band->index >= ARRAY_SIZE(bands))
+ return -EINVAL;
+
+ *band = bands[band->index];
+
+ return 0;
+}
+
+static int vidioc_g_tuner(struct file *file, void *priv,
+ struct v4l2_tuner *v)
+{
+ struct raremono_device *radio = video_drvdata(file);
+ int ret;
+
+ if (v->index > 0)
+ return -EINVAL;
+
+ strscpy(v->name, "AM/FM/SW", sizeof(v->name));
+ v->capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO |
+ V4L2_TUNER_CAP_FREQ_BANDS;
+ v->rangelow = AM_FREQ_RANGE_LOW * 16;
+ v->rangehigh = FM_FREQ_RANGE_HIGH * 16;
+ v->rxsubchans = V4L2_TUNER_SUB_STEREO | V4L2_TUNER_SUB_MONO;
+ v->audmode = (radio->curfreq < FM_FREQ_RANGE_LOW) ?
+ V4L2_TUNER_MODE_MONO : V4L2_TUNER_MODE_STEREO;
+ memset(radio->buffer, 1, BUFFER_LENGTH);
+ ret = usb_control_msg(radio->usbdev, usb_rcvctrlpipe(radio->usbdev, 0),
+ 1, 0xa1, 0x030d, 2, radio->buffer, BUFFER_LENGTH, USB_TIMEOUT);
+
+ if (ret < 0) {
+ dev_warn(radio->v4l2_dev.dev, "%s failed (%d)\n", __func__, ret);
+ return ret;
+ }
+ v->signal = ((radio->buffer[1] & 0xf) << 8 | radio->buffer[2]) << 4;
+ return 0;
+}
+
+static int vidioc_s_tuner(struct file *file, void *priv,
+ const struct v4l2_tuner *v)
+{
+ return v->index ? -EINVAL : 0;
+}
+
+static int vidioc_s_frequency(struct file *file, void *priv,
+ const struct v4l2_frequency *f)
+{
+ struct raremono_device *radio = video_drvdata(file);
+ u32 freq;
+ unsigned band;
+
+ if (f->tuner != 0 || f->type != V4L2_TUNER_RADIO)
+ return -EINVAL;
+
+ if (f->frequency >= (FM_FREQ_RANGE_LOW + SW_FREQ_RANGE_HIGH) * 8)
+ band = BAND_FM;
+ else if (f->frequency <= (AM_FREQ_RANGE_HIGH + SW_FREQ_RANGE_LOW) * 8)
+ band = BAND_AM;
+ else
+ band = BAND_SW;
+
+ freq = clamp_t(u32, f->frequency, bands[band].rangelow, bands[band].rangehigh);
+ return raremono_cmd_main(radio, band, freq / 16);
+}
+
+static int vidioc_g_frequency(struct file *file, void *priv,
+ struct v4l2_frequency *f)
+{
+ struct raremono_device *radio = video_drvdata(file);
+
+ if (f->tuner != 0)
+ return -EINVAL;
+ f->type = V4L2_TUNER_RADIO;
+ f->frequency = radio->curfreq * 16;
+ return 0;
+}
+
+static void raremono_device_release(struct v4l2_device *v4l2_dev)
+{
+ struct raremono_device *radio = to_raremono_dev(v4l2_dev);
+
+ kfree(radio->buffer);
+ kfree(radio);
+}
+
+/* File system interface */
+static const struct v4l2_file_operations usb_raremono_fops = {
+ .owner = THIS_MODULE,
+ .open = v4l2_fh_open,
+ .release = v4l2_fh_release,
+ .unlocked_ioctl = video_ioctl2,
+};
+
+static const struct v4l2_ioctl_ops usb_raremono_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_enum_freq_bands = vidioc_enum_freq_bands,
+};
+
+/* check if the device is present and register with v4l and usb if it is */
+static int usb_raremono_probe(struct usb_interface *intf,
+ const struct usb_device_id *id)
+{
+ struct raremono_device *radio;
+ int retval = 0;
+
+ radio = kzalloc_obj(*radio);
+ if (!radio)
+ return -ENOMEM;
+ radio->buffer = kmalloc(BUFFER_LENGTH, GFP_KERNEL);
+ if (!radio->buffer) {
+ kfree(radio);
+ return -ENOMEM;
+ }
+
+ radio->usbdev = interface_to_usbdev(intf);
+ radio->intf = intf;
+
+ /*
+ * This device uses the same USB IDs as the si470x SiLabs reference
+ * design. So do an additional check: attempt to read the device ID
+ * from the si470x: the lower 12 bits are 0x0242 for the si470x. The
+ * Raremono always returns 0x0800 (the meaning of that is unknown, but
+ * at least it works).
+ *
+ * We use this check to determine which device we are dealing with.
+ */
+ msleep(20);
+ retval = usb_control_msg(radio->usbdev,
+ usb_rcvctrlpipe(radio->usbdev, 0),
+ HID_REQ_GET_REPORT,
+ USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_IN,
+ 1, 2,
+ radio->buffer, 3, 500);
+ if (retval != 3 ||
+ (get_unaligned_be16(&radio->buffer[1]) & 0xfff) == 0x0242) {
+ dev_info(&intf->dev, "this is not Thanko's Raremono.\n");
+ retval = -ENODEV;
+ goto free_mem;
+ }
+
+ dev_info(&intf->dev, "Thanko's Raremono connected: (%04X:%04X)\n",
+ id->idVendor, id->idProduct);
+
+ retval = v4l2_device_register(&intf->dev, &radio->v4l2_dev);
+ if (retval < 0) {
+ dev_err(&intf->dev, "couldn't register v4l2_device\n");
+ goto free_mem;
+ }
+
+ mutex_init(&radio->lock);
+
+ strscpy(radio->vdev.name, radio->v4l2_dev.name,
+ sizeof(radio->vdev.name));
+ radio->vdev.v4l2_dev = &radio->v4l2_dev;
+ radio->vdev.fops = &usb_raremono_fops;
+ radio->vdev.ioctl_ops = &usb_raremono_ioctl_ops;
+ radio->vdev.lock = &radio->lock;
+ radio->vdev.release = video_device_release_empty;
+ radio->vdev.device_caps = V4L2_CAP_TUNER | V4L2_CAP_RADIO;
+ radio->v4l2_dev.release = raremono_device_release;
+
+ usb_set_intfdata(intf, &radio->v4l2_dev);
+
+ video_set_drvdata(&radio->vdev, radio);
+
+ raremono_cmd_main(radio, BAND_FM, 95160);
+
+ retval = video_register_device(&radio->vdev, VFL_TYPE_RADIO, -1);
+ if (retval == 0) {
+ dev_info(&intf->dev, "V4L2 device registered as %s\n",
+ video_device_node_name(&radio->vdev));
+ return 0;
+ }
+ dev_err(&intf->dev, "could not register video device\n");
+ v4l2_device_unregister(&radio->v4l2_dev);
+
+free_mem:
+ kfree(radio->buffer);
+ kfree(radio);
+ return retval;
+}
+
+/* USB subsystem interface */
+static struct usb_driver usb_raremono_driver = {
+ .name = "radio-raremono",
+ .probe = usb_raremono_probe,
+ .disconnect = usb_raremono_disconnect,
+ .id_table = usb_raremono_device_table,
+};
+
+module_usb_driver(usb_raremono_driver);
diff --git a/drivers/media/radio/radio-rtrack2.c b/drivers/media/radio/radio-rtrack2.c
new file mode 100644
index 000000000..289144690
--- /dev/null
+++ b/drivers/media/radio/radio-rtrack2.c
@@ -0,0 +1,142 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * RadioTrack II driver
+ * Copyright 1998 Ben Pfaff
+ *
+ * Based on RadioTrack I/RadioReveal (C) 1997 M. Kirkwood
+ * Converted to new API by Alan Cox <alan@lxorguk.ukuu.org.uk>
+ * Various bugfixes and enhancements by Russell Kroll <rkroll@exploits.org>
+ *
+ * Converted to the radio-isa framework by Hans Verkuil <hverkuil@kernel.org>
+ * Converted to V4L2 API by Mauro Carvalho Chehab <mchehab@kernel.org>
+ *
+ * Fully tested with actual hardware and the v4l2-compliance tool.
+ */
+
+#include <linux/module.h> /* Modules */
+#include <linux/init.h> /* Initdata */
+#include <linux/ioport.h> /* request_region */
+#include <linux/delay.h> /* udelay */
+#include <linux/videodev2.h> /* kernel radio structs */
+#include <linux/mutex.h>
+#include <linux/io.h> /* outb, outb_p */
+#include <linux/slab.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-ioctl.h>
+#include "radio-isa.h"
+
+MODULE_AUTHOR("Ben Pfaff");
+MODULE_DESCRIPTION("A driver for the RadioTrack II radio card.");
+MODULE_LICENSE("GPL");
+MODULE_VERSION("0.1.99");
+
+#ifndef CONFIG_RADIO_RTRACK2_PORT
+#define CONFIG_RADIO_RTRACK2_PORT -1
+#endif
+
+#define RTRACK2_MAX 2
+
+static int io[RTRACK2_MAX] = { [0] = CONFIG_RADIO_RTRACK2_PORT,
+ [1 ... (RTRACK2_MAX - 1)] = -1 };
+static int radio_nr[RTRACK2_MAX] = { [0 ... (RTRACK2_MAX - 1)] = -1 };
+
+module_param_array(io, int, NULL, 0444);
+MODULE_PARM_DESC(io, "I/O addresses of the RadioTrack card (0x20f or 0x30f)");
+module_param_array(radio_nr, int, NULL, 0444);
+MODULE_PARM_DESC(radio_nr, "Radio device numbers");
+
+static struct radio_isa_card *rtrack2_alloc(void)
+{
+ return kzalloc_obj(struct radio_isa_card);
+}
+
+static void zero(struct radio_isa_card *isa)
+{
+ outb_p(1, isa->io);
+ outb_p(3, isa->io);
+ outb_p(1, isa->io);
+}
+
+static void one(struct radio_isa_card *isa)
+{
+ outb_p(5, isa->io);
+ outb_p(7, isa->io);
+ outb_p(5, isa->io);
+}
+
+static int rtrack2_s_frequency(struct radio_isa_card *isa, u32 freq)
+{
+ int i;
+
+ freq = freq / 200 + 856;
+
+ outb_p(0xc8, isa->io);
+ outb_p(0xc9, isa->io);
+ outb_p(0xc9, isa->io);
+
+ for (i = 0; i < 10; i++)
+ zero(isa);
+
+ for (i = 14; i >= 0; i--)
+ if (freq & (1 << i))
+ one(isa);
+ else
+ zero(isa);
+
+ outb_p(0xc8, isa->io);
+ outb_p(v4l2_ctrl_g_ctrl(isa->mute), isa->io);
+ return 0;
+}
+
+static u32 rtrack2_g_signal(struct radio_isa_card *isa)
+{
+ /* bit set = no signal present */
+ return (inb(isa->io) & 2) ? 0 : 0xffff;
+}
+
+static int rtrack2_s_mute_volume(struct radio_isa_card *isa, bool mute, int vol)
+{
+ outb(mute, isa->io);
+ return 0;
+}
+
+static const struct radio_isa_ops rtrack2_ops = {
+ .alloc = rtrack2_alloc,
+ .s_mute_volume = rtrack2_s_mute_volume,
+ .s_frequency = rtrack2_s_frequency,
+ .g_signal = rtrack2_g_signal,
+};
+
+static const int rtrack2_ioports[] = { 0x20f, 0x30f };
+
+static struct radio_isa_driver rtrack2_driver = {
+ .driver = {
+ .match = radio_isa_match,
+ .probe = radio_isa_probe,
+ .remove = radio_isa_remove,
+ .driver = {
+ .name = "radio-rtrack2",
+ },
+ },
+ .io_params = io,
+ .radio_nr_params = radio_nr,
+ .io_ports = rtrack2_ioports,
+ .num_of_io_ports = ARRAY_SIZE(rtrack2_ioports),
+ .region_size = 4,
+ .card = "AIMSlab RadioTrack II",
+ .ops = &rtrack2_ops,
+ .has_stereo = true,
+};
+
+static int __init rtrack2_init(void)
+{
+ return isa_register_driver(&rtrack2_driver.driver, RTRACK2_MAX);
+}
+
+static void __exit rtrack2_exit(void)
+{
+ isa_unregister_driver(&rtrack2_driver.driver);
+}
+
+module_init(rtrack2_init);
+module_exit(rtrack2_exit);
diff --git a/drivers/media/radio/radio-sf16fmi.c b/drivers/media/radio/radio-sf16fmi.c
new file mode 100644
index 000000000..1fb88c2b9
--- /dev/null
+++ b/drivers/media/radio/radio-sf16fmi.c
@@ -0,0 +1,382 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* SF16-FMI, SF16-FMP and SF16-FMD radio driver for Linux radio support
+ * heavily based on rtrack driver...
+ * (c) 1997 M. Kirkwood
+ * (c) 1998 Petr Vandrovec, vandrove@vc.cvut.cz
+ *
+ * Fitted to new interface by Alan Cox <alan@lxorguk.ukuu.org.uk>
+ * Made working and cleaned up functions <mikael.hedin@irf.se>
+ * Support for ISAPnP by Ladislav Michl <ladis@psi.cz>
+ *
+ * Notes on the hardware
+ *
+ * Frequency control is done digitally -- ie out(port,encodefreq(95.8));
+ * No volume control - only mute/unmute - you have to use line volume
+ * control on SB-part of SF16-FMI/SF16-FMP/SF16-FMD
+ *
+ * Converted to V4L2 API by Mauro Carvalho Chehab <mchehab@kernel.org>
+ */
+
+#include <linux/kernel.h> /* __setup */
+#include <linux/module.h> /* Modules */
+#include <linux/init.h> /* Initdata */
+#include <linux/ioport.h> /* request_region */
+#include <linux/delay.h> /* udelay */
+#include <linux/isapnp.h>
+#include <linux/mutex.h>
+#include <linux/videodev2.h> /* kernel radio structs */
+#include <linux/io.h> /* outb, outb_p */
+#include <media/v4l2-device.h>
+#include <media/v4l2-ioctl.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-event.h>
+#include "lm7000.h"
+
+MODULE_AUTHOR("Petr Vandrovec, vandrove@vc.cvut.cz and M. Kirkwood");
+MODULE_DESCRIPTION("A driver for the SF16-FMI, SF16-FMP and SF16-FMD radio.");
+MODULE_LICENSE("GPL");
+MODULE_VERSION("0.0.3");
+
+static int io = -1;
+static int radio_nr = -1;
+
+module_param(io, int, 0);
+MODULE_PARM_DESC(io, "I/O address of the SF16-FMI/SF16-FMP/SF16-FMD card (0x284 or 0x384)");
+module_param(radio_nr, int, 0);
+
+struct fmi
+{
+ struct v4l2_device v4l2_dev;
+ struct v4l2_ctrl_handler hdl;
+ struct video_device vdev;
+ int io;
+ bool mute;
+ u32 curfreq; /* freq in kHz */
+ struct mutex lock;
+};
+
+static struct fmi fmi_card;
+static struct pnp_dev *dev;
+static bool pnp_attached;
+
+#define RSF16_MINFREQ (87U * 16000)
+#define RSF16_MAXFREQ (108U * 16000)
+
+#define FMI_BIT_TUN_CE (1 << 0)
+#define FMI_BIT_TUN_CLK (1 << 1)
+#define FMI_BIT_TUN_DATA (1 << 2)
+#define FMI_BIT_VOL_SW (1 << 3)
+#define FMI_BIT_TUN_STRQ (1 << 4)
+
+static void fmi_set_pins(void *handle, u8 pins)
+{
+ struct fmi *fmi = handle;
+ u8 bits = FMI_BIT_TUN_STRQ;
+
+ if (!fmi->mute)
+ bits |= FMI_BIT_VOL_SW;
+
+ if (pins & LM7000_DATA)
+ bits |= FMI_BIT_TUN_DATA;
+ if (pins & LM7000_CLK)
+ bits |= FMI_BIT_TUN_CLK;
+ if (pins & LM7000_CE)
+ bits |= FMI_BIT_TUN_CE;
+
+ mutex_lock(&fmi->lock);
+ outb_p(bits, fmi->io);
+ mutex_unlock(&fmi->lock);
+}
+
+static inline void fmi_mute(struct fmi *fmi)
+{
+ mutex_lock(&fmi->lock);
+ outb(0x00, fmi->io);
+ mutex_unlock(&fmi->lock);
+}
+
+static inline void fmi_unmute(struct fmi *fmi)
+{
+ mutex_lock(&fmi->lock);
+ outb(0x08, fmi->io);
+ mutex_unlock(&fmi->lock);
+}
+
+static inline int fmi_getsigstr(struct fmi *fmi)
+{
+ int val;
+ int res;
+
+ mutex_lock(&fmi->lock);
+ val = fmi->mute ? 0x00 : 0x08; /* mute/unmute */
+ outb(val, fmi->io);
+ outb(val | 0x10, fmi->io);
+ msleep(143); /* was schedule_timeout(HZ/7) */
+ res = (int)inb(fmi->io + 1);
+ outb(val, fmi->io);
+
+ mutex_unlock(&fmi->lock);
+ return (res & 2) ? 0 : 0xFFFF;
+}
+
+static void fmi_set_freq(struct fmi *fmi)
+{
+ fmi->curfreq = clamp(fmi->curfreq, RSF16_MINFREQ, RSF16_MAXFREQ);
+ /* rounding in steps of 800 to match the freq
+ that will be used */
+ lm7000_set_freq((fmi->curfreq / 800) * 800, fmi, fmi_set_pins);
+}
+
+static int vidioc_querycap(struct file *file, void *priv,
+ struct v4l2_capability *v)
+{
+ strscpy(v->driver, "radio-sf16fmi", sizeof(v->driver));
+ strscpy(v->card, "SF16-FMI/FMP/FMD radio", sizeof(v->card));
+ strscpy(v->bus_info, "ISA:radio-sf16fmi", sizeof(v->bus_info));
+ return 0;
+}
+
+static int vidioc_g_tuner(struct file *file, void *priv,
+ struct v4l2_tuner *v)
+{
+ struct fmi *fmi = video_drvdata(file);
+
+ if (v->index > 0)
+ return -EINVAL;
+
+ strscpy(v->name, "FM", sizeof(v->name));
+ v->type = V4L2_TUNER_RADIO;
+ v->rangelow = RSF16_MINFREQ;
+ v->rangehigh = RSF16_MAXFREQ;
+ v->rxsubchans = V4L2_TUNER_SUB_MONO | V4L2_TUNER_SUB_STEREO;
+ v->capability = V4L2_TUNER_CAP_STEREO | V4L2_TUNER_CAP_LOW;
+ v->audmode = V4L2_TUNER_MODE_STEREO;
+ v->signal = fmi_getsigstr(fmi);
+ return 0;
+}
+
+static int vidioc_s_tuner(struct file *file, void *priv,
+ const struct v4l2_tuner *v)
+{
+ return v->index ? -EINVAL : 0;
+}
+
+static int vidioc_s_frequency(struct file *file, void *priv,
+ const struct v4l2_frequency *f)
+{
+ struct fmi *fmi = video_drvdata(file);
+
+ if (f->tuner != 0 || f->type != V4L2_TUNER_RADIO)
+ return -EINVAL;
+
+ fmi->curfreq = f->frequency;
+ fmi_set_freq(fmi);
+
+ return 0;
+}
+
+static int vidioc_g_frequency(struct file *file, void *priv,
+ struct v4l2_frequency *f)
+{
+ struct fmi *fmi = video_drvdata(file);
+
+ if (f->tuner != 0)
+ return -EINVAL;
+ f->type = V4L2_TUNER_RADIO;
+ f->frequency = fmi->curfreq;
+ return 0;
+}
+
+static int fmi_s_ctrl(struct v4l2_ctrl *ctrl)
+{
+ struct fmi *fmi = container_of(ctrl->handler, struct fmi, hdl);
+
+ switch (ctrl->id) {
+ case V4L2_CID_AUDIO_MUTE:
+ if (ctrl->val)
+ fmi_mute(fmi);
+ else
+ fmi_unmute(fmi);
+ fmi->mute = ctrl->val;
+ return 0;
+ }
+ return -EINVAL;
+}
+
+static const struct v4l2_ctrl_ops fmi_ctrl_ops = {
+ .s_ctrl = fmi_s_ctrl,
+};
+
+static const struct v4l2_file_operations fmi_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 fmi_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,
+};
+
+/* ladis: this is my card. does any other types exist? */
+static struct isapnp_device_id id_table[] = {
+ /* SF16-FMI */
+ { ISAPNP_ANY_ID, ISAPNP_ANY_ID,
+ ISAPNP_VENDOR('M','F','R'), ISAPNP_FUNCTION(0xad10), 0},
+ /* SF16-FMD */
+ { ISAPNP_ANY_ID, ISAPNP_ANY_ID,
+ ISAPNP_VENDOR('M','F','R'), ISAPNP_FUNCTION(0xad12), 0},
+ { ISAPNP_CARD_END, },
+};
+
+MODULE_DEVICE_TABLE(isapnp, id_table);
+
+static int __init isapnp_fmi_probe(void)
+{
+ int i = 0;
+
+ while (id_table[i].card_vendor != 0 && dev == NULL) {
+ dev = pnp_find_dev(NULL, id_table[i].vendor,
+ id_table[i].function, NULL);
+ i++;
+ }
+
+ if (!dev)
+ return -ENODEV;
+ if (pnp_device_attach(dev) < 0)
+ return -EAGAIN;
+ if (pnp_activate_dev(dev) < 0) {
+ printk(KERN_ERR "radio-sf16fmi: PnP configure failed (out of resources?)\n");
+ pnp_device_detach(dev);
+ return -ENOMEM;
+ }
+ if (!pnp_port_valid(dev, 0)) {
+ pnp_device_detach(dev);
+ return -ENODEV;
+ }
+
+ i = pnp_port_start(dev, 0);
+ printk(KERN_INFO "radio-sf16fmi: PnP reports card at %#x\n", i);
+
+ return i;
+}
+
+static int __init fmi_init(void)
+{
+ struct fmi *fmi = &fmi_card;
+ struct v4l2_device *v4l2_dev = &fmi->v4l2_dev;
+ struct v4l2_ctrl_handler *hdl = &fmi->hdl;
+ int res, i;
+ static const int probe_ports[] = { 0, 0x284, 0x384 };
+
+ if (io < 0) {
+ for (i = 0; i < ARRAY_SIZE(probe_ports); i++) {
+ io = probe_ports[i];
+ if (io == 0) {
+ io = isapnp_fmi_probe();
+ if (io < 0)
+ continue;
+ pnp_attached = true;
+ }
+ if (!request_region(io, 2, "radio-sf16fmi")) {
+ if (pnp_attached)
+ pnp_device_detach(dev);
+ io = -1;
+ continue;
+ }
+ if (pnp_attached ||
+ ((inb(io) & 0xf9) == 0xf9 && (inb(io) & 0x4) == 0))
+ break;
+ release_region(io, 2);
+ io = -1;
+ }
+ } else {
+ if (!request_region(io, 2, "radio-sf16fmi")) {
+ printk(KERN_ERR "radio-sf16fmi: port %#x already in use\n", io);
+ return -EBUSY;
+ }
+ if (inb(io) == 0xff) {
+ printk(KERN_ERR "radio-sf16fmi: card not present at %#x\n", io);
+ release_region(io, 2);
+ return -ENODEV;
+ }
+ }
+ if (io < 0) {
+ printk(KERN_ERR "radio-sf16fmi: no cards found\n");
+ return -ENODEV;
+ }
+
+ strscpy(v4l2_dev->name, "sf16fmi", sizeof(v4l2_dev->name));
+ fmi->io = io;
+
+ res = v4l2_device_register(NULL, v4l2_dev);
+ if (res < 0) {
+ release_region(fmi->io, 2);
+ if (pnp_attached)
+ pnp_device_detach(dev);
+ v4l2_err(v4l2_dev, "Could not register v4l2_device\n");
+ return res;
+ }
+
+ v4l2_ctrl_handler_init(hdl, 1);
+ v4l2_ctrl_new_std(hdl, &fmi_ctrl_ops,
+ V4L2_CID_AUDIO_MUTE, 0, 1, 1, 1);
+ v4l2_dev->ctrl_handler = hdl;
+ if (hdl->error) {
+ res = hdl->error;
+ v4l2_err(v4l2_dev, "Could not register controls\n");
+ v4l2_ctrl_handler_free(hdl);
+ v4l2_device_unregister(v4l2_dev);
+ return res;
+ }
+
+ strscpy(fmi->vdev.name, v4l2_dev->name, sizeof(fmi->vdev.name));
+ fmi->vdev.v4l2_dev = v4l2_dev;
+ fmi->vdev.fops = &fmi_fops;
+ fmi->vdev.ioctl_ops = &fmi_ioctl_ops;
+ fmi->vdev.release = video_device_release_empty;
+ fmi->vdev.device_caps = V4L2_CAP_TUNER | V4L2_CAP_RADIO;
+ video_set_drvdata(&fmi->vdev, fmi);
+
+ mutex_init(&fmi->lock);
+
+ /* mute card and set default frequency */
+ fmi->mute = true;
+ fmi->curfreq = RSF16_MINFREQ;
+ fmi_set_freq(fmi);
+
+ if (video_register_device(&fmi->vdev, VFL_TYPE_RADIO, radio_nr) < 0) {
+ v4l2_ctrl_handler_free(hdl);
+ v4l2_device_unregister(v4l2_dev);
+ release_region(fmi->io, 2);
+ if (pnp_attached)
+ pnp_device_detach(dev);
+ return -EINVAL;
+ }
+
+ v4l2_info(v4l2_dev, "card driver at 0x%x\n", fmi->io);
+ return 0;
+}
+
+static void __exit fmi_exit(void)
+{
+ struct fmi *fmi = &fmi_card;
+
+ v4l2_ctrl_handler_free(&fmi->hdl);
+ video_unregister_device(&fmi->vdev);
+ v4l2_device_unregister(&fmi->v4l2_dev);
+ release_region(fmi->io, 2);
+ if (dev && pnp_attached)
+ pnp_device_detach(dev);
+}
+
+module_init(fmi_init);
+module_exit(fmi_exit);
diff --git a/drivers/media/radio/radio-sf16fmr2.c b/drivers/media/radio/radio-sf16fmr2.c
new file mode 100644
index 000000000..2cc91e576
--- /dev/null
+++ b/drivers/media/radio/radio-sf16fmr2.c
@@ -0,0 +1,345 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* SF16-FMR2 and SF16-FMD2 radio driver for Linux
+ * Copyright (c) 2011 Ondrej Zary
+ *
+ * Original driver was (c) 2000-2002 Ziglio Frediano, freddy77@angelfire.com
+ * but almost nothing remained here after conversion to generic TEA575x
+ * implementation
+ */
+
+#include <linux/delay.h>
+#include <linux/module.h> /* Modules */
+#include <linux/init.h> /* Initdata */
+#include <linux/slab.h>
+#include <linux/ioport.h> /* request_region */
+#include <linux/io.h> /* outb, outb_p */
+#include <linux/isa.h>
+#include <linux/pnp.h>
+#include <media/drv-intf/tea575x.h>
+
+MODULE_AUTHOR("Ondrej Zary");
+MODULE_DESCRIPTION("MediaForte SF16-FMR2 and SF16-FMD2 FM radio card driver");
+MODULE_LICENSE("GPL");
+
+/* these cards can only use two different ports (0x384 and 0x284) */
+#define FMR2_MAX 2
+
+static int radio_nr[FMR2_MAX] = { [0 ... (FMR2_MAX - 1)] = -1 };
+module_param_array(radio_nr, int, NULL, 0444);
+MODULE_PARM_DESC(radio_nr, "Radio device numbers");
+
+struct fmr2 {
+ int io;
+ struct v4l2_device v4l2_dev;
+ struct snd_tea575x tea;
+ struct v4l2_ctrl *volume;
+ struct v4l2_ctrl *balance;
+ bool is_fmd2;
+};
+
+static int num_fmr2_cards;
+static struct fmr2 *fmr2_cards[FMR2_MAX];
+static bool isa_registered;
+static bool pnp_registered;
+
+/* the port is hardwired on SF16-FMR2 */
+#define FMR2_PORT 0x384
+
+/* TEA575x tuner pins */
+#define STR_DATA (1 << 0)
+#define STR_CLK (1 << 1)
+#define STR_WREN (1 << 2)
+#define STR_MOST (1 << 3)
+/* PT2254A/TC9154A volume control pins */
+#define PT_ST (1 << 4)
+#define PT_CK (1 << 5)
+#define PT_DATA (1 << 6)
+/* volume control presence pin */
+#define FMR2_HASVOL (1 << 7)
+
+static void fmr2_tea575x_set_pins(struct snd_tea575x *tea, u8 pins)
+{
+ struct fmr2 *fmr2 = tea->private_data;
+ u8 bits = 0;
+
+ bits |= (pins & TEA575X_DATA) ? STR_DATA : 0;
+ bits |= (pins & TEA575X_CLK) ? STR_CLK : 0;
+ /* WRITE_ENABLE is inverted, DATA must be high during read */
+ bits |= (pins & TEA575X_WREN) ? 0 : STR_WREN | STR_DATA;
+
+ outb(bits, fmr2->io);
+}
+
+static u8 fmr2_tea575x_get_pins(struct snd_tea575x *tea)
+{
+ struct fmr2 *fmr2 = tea->private_data;
+ u8 bits = inb(fmr2->io);
+
+ return ((bits & STR_DATA) ? TEA575X_DATA : 0) |
+ ((bits & STR_MOST) ? TEA575X_MOST : 0);
+}
+
+static void fmr2_tea575x_set_direction(struct snd_tea575x *tea, bool output)
+{
+}
+
+static const struct snd_tea575x_ops fmr2_tea_ops = {
+ .set_pins = fmr2_tea575x_set_pins,
+ .get_pins = fmr2_tea575x_get_pins,
+ .set_direction = fmr2_tea575x_set_direction,
+};
+
+/* TC9154A/PT2254A volume control */
+
+/* 18-bit shift register bit definitions */
+#define TC9154A_ATT_MAJ_0DB (1 << 0)
+#define TC9154A_ATT_MAJ_10DB (1 << 1)
+#define TC9154A_ATT_MAJ_20DB (1 << 2)
+#define TC9154A_ATT_MAJ_30DB (1 << 3)
+#define TC9154A_ATT_MAJ_40DB (1 << 4)
+#define TC9154A_ATT_MAJ_50DB (1 << 5)
+#define TC9154A_ATT_MAJ_60DB (1 << 6)
+
+#define TC9154A_ATT_MIN_0DB (1 << 7)
+#define TC9154A_ATT_MIN_2DB (1 << 8)
+#define TC9154A_ATT_MIN_4DB (1 << 9)
+#define TC9154A_ATT_MIN_6DB (1 << 10)
+#define TC9154A_ATT_MIN_8DB (1 << 11)
+/* bit 12 is ignored */
+#define TC9154A_CHANNEL_LEFT (1 << 13)
+#define TC9154A_CHANNEL_RIGHT (1 << 14)
+/* bits 15, 16, 17 must be 0 */
+
+#define TC9154A_ATT_MAJ(x) (1 << x)
+#define TC9154A_ATT_MIN(x) (1 << (7 + x))
+
+static void tc9154a_set_pins(struct fmr2 *fmr2, u8 pins)
+{
+ if (!fmr2->tea.mute)
+ pins |= STR_WREN;
+
+ outb(pins, fmr2->io);
+}
+
+static void tc9154a_set_attenuation(struct fmr2 *fmr2, int att, u32 channel)
+{
+ int i;
+ u32 reg;
+ u8 bit;
+
+ reg = TC9154A_ATT_MAJ(att / 10) | TC9154A_ATT_MIN((att % 10) / 2);
+ reg |= channel;
+ /* write 18-bit shift register, LSB first */
+ for (i = 0; i < 18; i++) {
+ bit = reg & (1 << i) ? PT_DATA : 0;
+ tc9154a_set_pins(fmr2, bit);
+ udelay(5);
+ tc9154a_set_pins(fmr2, bit | PT_CK);
+ udelay(5);
+ tc9154a_set_pins(fmr2, bit);
+ }
+
+ /* latch register data */
+ udelay(5);
+ tc9154a_set_pins(fmr2, PT_ST);
+ udelay(5);
+ tc9154a_set_pins(fmr2, 0);
+}
+
+static int fmr2_s_ctrl(struct v4l2_ctrl *ctrl)
+{
+ struct snd_tea575x *tea = container_of(ctrl->handler, struct snd_tea575x, ctrl_handler);
+ struct fmr2 *fmr2 = tea->private_data;
+ int volume, balance, left, right;
+
+ switch (ctrl->id) {
+ case V4L2_CID_AUDIO_VOLUME:
+ volume = ctrl->val;
+ balance = fmr2->balance->cur.val;
+ break;
+ case V4L2_CID_AUDIO_BALANCE:
+ balance = ctrl->val;
+ volume = fmr2->volume->cur.val;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ left = right = volume;
+ if (balance < 0)
+ right = max(0, right + balance);
+ if (balance > 0)
+ left = max(0, left - balance);
+
+ tc9154a_set_attenuation(fmr2, abs(left - 68), TC9154A_CHANNEL_LEFT);
+ tc9154a_set_attenuation(fmr2, abs(right - 68), TC9154A_CHANNEL_RIGHT);
+
+ return 0;
+}
+
+static const struct v4l2_ctrl_ops fmr2_ctrl_ops = {
+ .s_ctrl = fmr2_s_ctrl,
+};
+
+static int fmr2_tea_ext_init(struct snd_tea575x *tea)
+{
+ struct fmr2 *fmr2 = tea->private_data;
+
+ /* FMR2 can have volume control, FMD2 can't (uses SB16 mixer) */
+ if (!fmr2->is_fmd2 && inb(fmr2->io) & FMR2_HASVOL) {
+ fmr2->volume = v4l2_ctrl_new_std(&tea->ctrl_handler, &fmr2_ctrl_ops, V4L2_CID_AUDIO_VOLUME, 0, 68, 2, 56);
+ fmr2->balance = v4l2_ctrl_new_std(&tea->ctrl_handler, &fmr2_ctrl_ops, V4L2_CID_AUDIO_BALANCE, -68, 68, 2, 0);
+ if (tea->ctrl_handler.error) {
+ printk(KERN_ERR "radio-sf16fmr2: can't initialize controls\n");
+ return tea->ctrl_handler.error;
+ }
+ }
+
+ return 0;
+}
+
+static const struct pnp_device_id fmr2_pnp_ids[] = {
+ { .id = "MFRad13" }, /* tuner subdevice of SF16-FMD2 */
+ { .id = "" }
+};
+MODULE_DEVICE_TABLE(pnp, fmr2_pnp_ids);
+
+static int fmr2_probe(struct fmr2 *fmr2, struct device *pdev, int io)
+{
+ int err, i;
+ char *card_name = fmr2->is_fmd2 ? "SF16-FMD2" : "SF16-FMR2";
+
+ /* avoid errors if a card was already registered at given port */
+ for (i = 0; i < num_fmr2_cards; i++)
+ if (io == fmr2_cards[i]->io)
+ return -EBUSY;
+
+ strscpy(fmr2->v4l2_dev.name, "radio-sf16fmr2",
+ sizeof(fmr2->v4l2_dev.name));
+ fmr2->io = io;
+
+ if (!request_region(fmr2->io, 2, fmr2->v4l2_dev.name)) {
+ printk(KERN_ERR "radio-sf16fmr2: I/O port 0x%x already in use\n", fmr2->io);
+ return -EBUSY;
+ }
+
+ dev_set_drvdata(pdev, fmr2);
+ err = v4l2_device_register(pdev, &fmr2->v4l2_dev);
+ if (err < 0) {
+ v4l2_err(&fmr2->v4l2_dev, "Could not register v4l2_device\n");
+ release_region(fmr2->io, 2);
+ return err;
+ }
+ fmr2->tea.v4l2_dev = &fmr2->v4l2_dev;
+ fmr2->tea.private_data = fmr2;
+ fmr2->tea.radio_nr = radio_nr[num_fmr2_cards];
+ fmr2->tea.ops = &fmr2_tea_ops;
+ fmr2->tea.ext_init = fmr2_tea_ext_init;
+ strscpy(fmr2->tea.card, card_name, sizeof(fmr2->tea.card));
+ snprintf(fmr2->tea.bus_info, sizeof(fmr2->tea.bus_info), "%s:%s",
+ fmr2->is_fmd2 ? "PnP" : "ISA", dev_name(pdev));
+
+ if (snd_tea575x_init(&fmr2->tea, THIS_MODULE)) {
+ printk(KERN_ERR "radio-sf16fmr2: Unable to detect TEA575x tuner\n");
+ release_region(fmr2->io, 2);
+ return -ENODEV;
+ }
+
+ printk(KERN_INFO "radio-sf16fmr2: %s radio card at 0x%x.\n",
+ card_name, fmr2->io);
+ return 0;
+}
+
+static int fmr2_isa_match(struct device *pdev, unsigned int ndev)
+{
+ struct fmr2 *fmr2 = kzalloc_obj(*fmr2);
+ if (!fmr2)
+ return 0;
+
+ if (fmr2_probe(fmr2, pdev, FMR2_PORT)) {
+ kfree(fmr2);
+ return 0;
+ }
+ dev_set_drvdata(pdev, fmr2);
+ fmr2_cards[num_fmr2_cards++] = fmr2;
+
+ return 1;
+}
+
+static int fmr2_pnp_probe(struct pnp_dev *pdev, const struct pnp_device_id *id)
+{
+ int ret;
+ struct fmr2 *fmr2 = kzalloc_obj(*fmr2);
+ if (!fmr2)
+ return -ENOMEM;
+
+ fmr2->is_fmd2 = true;
+ ret = fmr2_probe(fmr2, &pdev->dev, pnp_port_start(pdev, 0));
+ if (ret) {
+ kfree(fmr2);
+ return ret;
+ }
+ pnp_set_drvdata(pdev, fmr2);
+ fmr2_cards[num_fmr2_cards++] = fmr2;
+
+ return 0;
+}
+
+static void fmr2_remove(struct fmr2 *fmr2)
+{
+ snd_tea575x_exit(&fmr2->tea);
+ release_region(fmr2->io, 2);
+ v4l2_device_unregister(&fmr2->v4l2_dev);
+ kfree(fmr2);
+}
+
+static void fmr2_isa_remove(struct device *pdev, unsigned int ndev)
+{
+ fmr2_remove(dev_get_drvdata(pdev));
+}
+
+static void fmr2_pnp_remove(struct pnp_dev *pdev)
+{
+ fmr2_remove(pnp_get_drvdata(pdev));
+ pnp_set_drvdata(pdev, NULL);
+}
+
+static struct isa_driver fmr2_isa_driver = {
+ .match = fmr2_isa_match,
+ .remove = fmr2_isa_remove,
+ .driver = {
+ .name = "radio-sf16fmr2",
+ },
+};
+
+static struct pnp_driver fmr2_pnp_driver = {
+ .name = "radio-sf16fmr2",
+ .id_table = fmr2_pnp_ids,
+ .probe = fmr2_pnp_probe,
+ .remove = fmr2_pnp_remove,
+};
+
+static int __init fmr2_init(void)
+{
+ int ret;
+
+ ret = pnp_register_driver(&fmr2_pnp_driver);
+ if (!ret)
+ pnp_registered = true;
+ ret = isa_register_driver(&fmr2_isa_driver, 1);
+ if (!ret)
+ isa_registered = true;
+
+ return (pnp_registered || isa_registered) ? 0 : ret;
+}
+
+static void __exit fmr2_exit(void)
+{
+ if (pnp_registered)
+ pnp_unregister_driver(&fmr2_pnp_driver);
+ if (isa_registered)
+ isa_unregister_driver(&fmr2_isa_driver);
+}
+
+module_init(fmr2_init);
+module_exit(fmr2_exit);
diff --git a/drivers/media/radio/radio-shark.c b/drivers/media/radio/radio-shark.c
new file mode 100644
index 000000000..c1f356c5f
--- /dev/null
+++ b/drivers/media/radio/radio-shark.c
@@ -0,0 +1,429 @@
+/*
+ * Linux V4L2 radio driver for the Griffin radioSHARK USB radio receiver
+ *
+ * Note the radioSHARK offers the audio through a regular USB audio device,
+ * this driver only handles the tuning.
+ *
+ * The info necessary to drive the shark was taken from the small userspace
+ * shark.c program by Michael Rolig, which he kindly placed in the Public
+ * Domain.
+ *
+ * Copyright (c) 2012 Hans de Goede <hdegoede@redhat.com>
+ *
+ * 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.
+*/
+
+#include <linux/init.h>
+#include <linux/kernel.h>
+#include <linux/leds.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+#include <media/v4l2-device.h>
+#include <media/drv-intf/tea575x.h>
+
+#if defined(CONFIG_LEDS_CLASS) || \
+ (defined(CONFIG_LEDS_CLASS_MODULE) && defined(CONFIG_RADIO_SHARK_MODULE))
+#define SHARK_USE_LEDS 1
+#endif
+
+/*
+ * Version Information
+ */
+MODULE_AUTHOR("Hans de Goede <hdegoede@redhat.com>");
+MODULE_DESCRIPTION("Griffin radioSHARK, USB radio receiver driver");
+MODULE_LICENSE("GPL");
+
+#define SHARK_IN_EP 0x83
+#define SHARK_OUT_EP 0x05
+
+#define TEA575X_BIT_MONO (1<<22) /* 0 = stereo, 1 = mono */
+#define TEA575X_BIT_BAND_MASK (3<<20)
+#define TEA575X_BIT_BAND_FM (0<<20)
+
+#define TB_LEN 6
+#define DRV_NAME "radioshark"
+
+#define v4l2_dev_to_shark(d) container_of(d, struct shark_device, v4l2_dev)
+
+/* Note BLUE_IS_PULSE comes after NO_LEDS as it is a status bit, not a LED */
+enum { BLUE_LED, BLUE_PULSE_LED, RED_LED, NO_LEDS, BLUE_IS_PULSE };
+
+struct shark_device {
+ struct usb_device *usbdev;
+ struct v4l2_device v4l2_dev;
+ struct snd_tea575x tea;
+
+#ifdef SHARK_USE_LEDS
+ struct work_struct led_work;
+ struct led_classdev leds[NO_LEDS];
+ char led_names[NO_LEDS][32];
+ atomic_t brightness[NO_LEDS];
+ unsigned long brightness_new;
+#endif
+
+ u8 *transfer_buffer;
+ u32 last_val;
+};
+
+static atomic_t shark_instance = ATOMIC_INIT(0);
+
+static void shark_write_val(struct snd_tea575x *tea, u32 val)
+{
+ struct shark_device *shark = tea->private_data;
+ int i, res, actual_len;
+
+ /* Avoid unnecessary (slow) USB transfers */
+ if (shark->last_val == val)
+ return;
+
+ memset(shark->transfer_buffer, 0, TB_LEN);
+ shark->transfer_buffer[0] = 0xc0; /* Write shift register command */
+ for (i = 0; i < 4; i++)
+ shark->transfer_buffer[i] |= (val >> (24 - i * 8)) & 0xff;
+
+ res = usb_interrupt_msg(shark->usbdev,
+ usb_sndintpipe(shark->usbdev, SHARK_OUT_EP),
+ shark->transfer_buffer, TB_LEN,
+ &actual_len, 1000);
+ if (res >= 0)
+ shark->last_val = val;
+ else
+ v4l2_err(&shark->v4l2_dev, "set-freq error: %d\n", res);
+}
+
+static u32 shark_read_val(struct snd_tea575x *tea)
+{
+ struct shark_device *shark = tea->private_data;
+ int i, res, actual_len;
+ u32 val = 0;
+
+ memset(shark->transfer_buffer, 0, TB_LEN);
+ shark->transfer_buffer[0] = 0x80;
+ res = usb_interrupt_msg(shark->usbdev,
+ usb_sndintpipe(shark->usbdev, SHARK_OUT_EP),
+ shark->transfer_buffer, TB_LEN,
+ &actual_len, 1000);
+ if (res < 0) {
+ v4l2_err(&shark->v4l2_dev, "request-status error: %d\n", res);
+ return shark->last_val;
+ }
+
+ res = usb_interrupt_msg(shark->usbdev,
+ usb_rcvintpipe(shark->usbdev, SHARK_IN_EP),
+ shark->transfer_buffer, TB_LEN,
+ &actual_len, 1000);
+ if (res < 0) {
+ v4l2_err(&shark->v4l2_dev, "get-status error: %d\n", res);
+ return shark->last_val;
+ }
+
+ for (i = 0; i < 4; i++)
+ val |= shark->transfer_buffer[i] << (24 - i * 8);
+
+ shark->last_val = val;
+
+ /*
+ * The shark does not allow actually reading the stereo / mono pin :(
+ * So assume that when we're tuned to an FM station and mono has not
+ * been requested, that we're receiving stereo.
+ */
+ if (((val & TEA575X_BIT_BAND_MASK) == TEA575X_BIT_BAND_FM) &&
+ !(val & TEA575X_BIT_MONO))
+ shark->tea.stereo = true;
+ else
+ shark->tea.stereo = false;
+
+ return val;
+}
+
+static const struct snd_tea575x_ops shark_tea_ops = {
+ .write_val = shark_write_val,
+ .read_val = shark_read_val,
+};
+
+#ifdef SHARK_USE_LEDS
+static void shark_led_work(struct work_struct *work)
+{
+ struct shark_device *shark =
+ container_of(work, struct shark_device, led_work);
+ int i, res, brightness, actual_len;
+
+ for (i = 0; i < 3; i++) {
+ if (!test_and_clear_bit(i, &shark->brightness_new))
+ continue;
+
+ brightness = atomic_read(&shark->brightness[i]);
+ memset(shark->transfer_buffer, 0, TB_LEN);
+ if (i != RED_LED) {
+ shark->transfer_buffer[0] = 0xA0 + i;
+ shark->transfer_buffer[1] = brightness;
+ } else
+ shark->transfer_buffer[0] = brightness ? 0xA9 : 0xA8;
+ res = usb_interrupt_msg(shark->usbdev,
+ usb_sndintpipe(shark->usbdev, 0x05),
+ shark->transfer_buffer, TB_LEN,
+ &actual_len, 1000);
+ if (res < 0)
+ v4l2_err(&shark->v4l2_dev, "set LED %s error: %d\n",
+ shark->led_names[i], res);
+ }
+}
+
+static void shark_led_set_blue(struct led_classdev *led_cdev,
+ enum led_brightness value)
+{
+ struct shark_device *shark =
+ container_of(led_cdev, struct shark_device, leds[BLUE_LED]);
+
+ atomic_set(&shark->brightness[BLUE_LED], value);
+ set_bit(BLUE_LED, &shark->brightness_new);
+ clear_bit(BLUE_IS_PULSE, &shark->brightness_new);
+ schedule_work(&shark->led_work);
+}
+
+static void shark_led_set_blue_pulse(struct led_classdev *led_cdev,
+ enum led_brightness value)
+{
+ struct shark_device *shark = container_of(led_cdev,
+ struct shark_device, leds[BLUE_PULSE_LED]);
+
+ atomic_set(&shark->brightness[BLUE_PULSE_LED], 256 - value);
+ set_bit(BLUE_PULSE_LED, &shark->brightness_new);
+ set_bit(BLUE_IS_PULSE, &shark->brightness_new);
+ schedule_work(&shark->led_work);
+}
+
+static void shark_led_set_red(struct led_classdev *led_cdev,
+ enum led_brightness value)
+{
+ struct shark_device *shark =
+ container_of(led_cdev, struct shark_device, leds[RED_LED]);
+
+ atomic_set(&shark->brightness[RED_LED], value);
+ set_bit(RED_LED, &shark->brightness_new);
+ schedule_work(&shark->led_work);
+}
+
+static const struct led_classdev shark_led_templates[NO_LEDS] = {
+ [BLUE_LED] = {
+ .name = "%s:blue:",
+ .brightness = LED_OFF,
+ .max_brightness = 127,
+ .brightness_set = shark_led_set_blue,
+ },
+ [BLUE_PULSE_LED] = {
+ .name = "%s:blue-pulse:",
+ .brightness = LED_OFF,
+ .max_brightness = 255,
+ .brightness_set = shark_led_set_blue_pulse,
+ },
+ [RED_LED] = {
+ .name = "%s:red:",
+ .brightness = LED_OFF,
+ .max_brightness = 1,
+ .brightness_set = shark_led_set_red,
+ },
+};
+
+static int shark_register_leds(struct shark_device *shark, struct device *dev)
+{
+ int i, retval;
+
+ atomic_set(&shark->brightness[BLUE_LED], 127);
+ INIT_WORK(&shark->led_work, shark_led_work);
+ for (i = 0; i < NO_LEDS; i++) {
+ shark->leds[i] = shark_led_templates[i];
+ snprintf(shark->led_names[i], sizeof(shark->led_names[0]),
+ shark->leds[i].name, shark->v4l2_dev.name);
+ shark->leds[i].name = shark->led_names[i];
+ retval = led_classdev_register(dev, &shark->leds[i]);
+ if (retval) {
+ v4l2_err(&shark->v4l2_dev,
+ "couldn't register led: %s\n",
+ shark->led_names[i]);
+ return retval;
+ }
+ }
+ return 0;
+}
+
+static void shark_unregister_leds(struct shark_device *shark)
+{
+ int i;
+
+ for (i = 0; i < NO_LEDS; i++)
+ led_classdev_unregister(&shark->leds[i]);
+
+ cancel_work_sync(&shark->led_work);
+}
+
+static inline void shark_resume_leds(struct shark_device *shark)
+{
+ if (test_bit(BLUE_IS_PULSE, &shark->brightness_new))
+ set_bit(BLUE_PULSE_LED, &shark->brightness_new);
+ else
+ set_bit(BLUE_LED, &shark->brightness_new);
+ set_bit(RED_LED, &shark->brightness_new);
+ schedule_work(&shark->led_work);
+}
+#else
+static int shark_register_leds(struct shark_device *shark, struct device *dev)
+{
+ v4l2_warn(&shark->v4l2_dev,
+ "CONFIG_LEDS_CLASS not enabled, LED support disabled\n");
+ return 0;
+}
+static inline void shark_unregister_leds(struct shark_device *shark) { }
+static inline void shark_resume_leds(struct shark_device *shark) { }
+#endif
+
+static void usb_shark_disconnect(struct usb_interface *intf)
+{
+ struct v4l2_device *v4l2_dev = usb_get_intfdata(intf);
+ struct shark_device *shark = v4l2_dev_to_shark(v4l2_dev);
+
+ mutex_lock(&shark->tea.mutex);
+ v4l2_device_disconnect(&shark->v4l2_dev);
+ snd_tea575x_exit(&shark->tea);
+ mutex_unlock(&shark->tea.mutex);
+
+ shark_unregister_leds(shark);
+
+ v4l2_device_put(&shark->v4l2_dev);
+}
+
+static void usb_shark_release(struct v4l2_device *v4l2_dev)
+{
+ struct shark_device *shark = v4l2_dev_to_shark(v4l2_dev);
+
+ v4l2_device_unregister(&shark->v4l2_dev);
+ kfree(shark->transfer_buffer);
+ kfree(shark);
+}
+
+static int usb_shark_probe(struct usb_interface *intf,
+ const struct usb_device_id *id)
+{
+ struct shark_device *shark;
+ int retval = -ENOMEM;
+ static const u8 ep_addresses[] = {
+ SHARK_IN_EP | USB_DIR_IN,
+ SHARK_OUT_EP | USB_DIR_OUT,
+ 0};
+
+ /* Are the expected endpoints present? */
+ if (!usb_check_int_endpoints(intf, ep_addresses)) {
+ dev_err(&intf->dev, "Invalid radioSHARK device\n");
+ return -EINVAL;
+ }
+
+ shark = kzalloc_obj(struct shark_device);
+ if (!shark)
+ return retval;
+
+ shark->transfer_buffer = kmalloc(TB_LEN, GFP_KERNEL);
+ if (!shark->transfer_buffer)
+ goto err_alloc_buffer;
+
+ v4l2_device_set_name(&shark->v4l2_dev, DRV_NAME, &shark_instance);
+
+ retval = shark_register_leds(shark, &intf->dev);
+ if (retval)
+ goto err_reg_leds;
+
+ shark->v4l2_dev.release = usb_shark_release;
+ retval = v4l2_device_register(&intf->dev, &shark->v4l2_dev);
+ if (retval) {
+ v4l2_err(&shark->v4l2_dev, "couldn't register v4l2_device\n");
+ goto err_reg_dev;
+ }
+
+ shark->usbdev = interface_to_usbdev(intf);
+ shark->tea.v4l2_dev = &shark->v4l2_dev;
+ shark->tea.private_data = shark;
+ shark->tea.radio_nr = -1;
+ shark->tea.ops = &shark_tea_ops;
+ shark->tea.cannot_mute = true;
+ shark->tea.has_am = true;
+ strscpy(shark->tea.card, "Griffin radioSHARK",
+ sizeof(shark->tea.card));
+ usb_make_path(shark->usbdev, shark->tea.bus_info,
+ sizeof(shark->tea.bus_info));
+
+ retval = snd_tea575x_init(&shark->tea, THIS_MODULE);
+ if (retval) {
+ v4l2_err(&shark->v4l2_dev, "couldn't init tea5757\n");
+ goto err_init_tea;
+ }
+
+ return 0;
+
+err_init_tea:
+ v4l2_device_unregister(&shark->v4l2_dev);
+err_reg_dev:
+ shark_unregister_leds(shark);
+err_reg_leds:
+ kfree(shark->transfer_buffer);
+err_alloc_buffer:
+ kfree(shark);
+
+ return retval;
+}
+
+#ifdef CONFIG_PM
+static int usb_shark_suspend(struct usb_interface *intf, pm_message_t message)
+{
+ return 0;
+}
+
+static int usb_shark_resume(struct usb_interface *intf)
+{
+ struct v4l2_device *v4l2_dev = usb_get_intfdata(intf);
+ struct shark_device *shark = v4l2_dev_to_shark(v4l2_dev);
+
+ mutex_lock(&shark->tea.mutex);
+ snd_tea575x_set_freq(&shark->tea);
+ mutex_unlock(&shark->tea.mutex);
+
+ shark_resume_leds(shark);
+
+ return 0;
+}
+#endif
+
+/* Specify the bcdDevice value, as the radioSHARK and radioSHARK2 share ids */
+static const struct usb_device_id usb_shark_device_table[] = {
+ { .match_flags = USB_DEVICE_ID_MATCH_DEVICE_AND_VERSION |
+ USB_DEVICE_ID_MATCH_INT_CLASS,
+ .idVendor = 0x077d,
+ .idProduct = 0x627a,
+ .bcdDevice_lo = 0x0001,
+ .bcdDevice_hi = 0x0001,
+ .bInterfaceClass = 3,
+ },
+ { }
+};
+MODULE_DEVICE_TABLE(usb, usb_shark_device_table);
+
+static struct usb_driver usb_shark_driver = {
+ .name = DRV_NAME,
+ .probe = usb_shark_probe,
+ .disconnect = usb_shark_disconnect,
+ .id_table = usb_shark_device_table,
+#ifdef CONFIG_PM
+ .suspend = usb_shark_suspend,
+ .resume = usb_shark_resume,
+ .reset_resume = usb_shark_resume,
+#endif
+};
+module_usb_driver(usb_shark_driver);
diff --git a/drivers/media/radio/radio-shark2.c b/drivers/media/radio/radio-shark2.c
new file mode 100644
index 000000000..7cc173e21
--- /dev/null
+++ b/drivers/media/radio/radio-shark2.c
@@ -0,0 +1,395 @@
+/*
+ * Linux V4L2 radio driver for the Griffin radioSHARK2 USB radio receiver
+ *
+ * Note the radioSHARK2 offers the audio through a regular USB audio device,
+ * this driver only handles the tuning.
+ *
+ * The info necessary to drive the shark2 was taken from the small userspace
+ * shark2.c program by Hisaaki Shibata, which he kindly placed in the Public
+ * Domain.
+ *
+ * Copyright (c) 2012 Hans de Goede <hdegoede@redhat.com>
+ *
+ * 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.
+ */
+
+#include <linux/init.h>
+#include <linux/kernel.h>
+#include <linux/leds.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+#include <media/v4l2-device.h>
+#include "radio-tea5777.h"
+
+#if defined(CONFIG_LEDS_CLASS) || \
+ (defined(CONFIG_LEDS_CLASS_MODULE) && defined(CONFIG_RADIO_SHARK2_MODULE))
+#define SHARK_USE_LEDS 1
+#endif
+
+MODULE_AUTHOR("Hans de Goede <hdegoede@redhat.com>");
+MODULE_DESCRIPTION("Griffin radioSHARK2, USB radio receiver driver");
+MODULE_LICENSE("GPL");
+
+static int debug;
+module_param(debug, int, 0);
+MODULE_PARM_DESC(debug, "Debug level (0-1)");
+
+#define SHARK_IN_EP 0x83
+#define SHARK_OUT_EP 0x05
+
+#define TB_LEN 7
+#define DRV_NAME "radioshark2"
+
+#define v4l2_dev_to_shark(d) container_of(d, struct shark_device, v4l2_dev)
+
+enum { BLUE_LED, RED_LED, NO_LEDS };
+
+struct shark_device {
+ struct usb_device *usbdev;
+ struct v4l2_device v4l2_dev;
+ struct radio_tea5777 tea;
+
+#ifdef SHARK_USE_LEDS
+ struct work_struct led_work;
+ struct led_classdev leds[NO_LEDS];
+ char led_names[NO_LEDS][64];
+ atomic_t brightness[NO_LEDS];
+ unsigned long brightness_new;
+#endif
+
+ u8 *transfer_buffer;
+};
+
+static atomic_t shark_instance = ATOMIC_INIT(0);
+
+static int shark_write_reg(struct radio_tea5777 *tea, u64 reg)
+{
+ struct shark_device *shark = tea->private_data;
+ int i, res, actual_len;
+
+ memset(shark->transfer_buffer, 0, TB_LEN);
+ shark->transfer_buffer[0] = 0x81; /* Write register command */
+ for (i = 0; i < 6; i++)
+ shark->transfer_buffer[i + 1] = (reg >> (40 - i * 8)) & 0xff;
+
+ v4l2_dbg(1, debug, tea->v4l2_dev, "shark2-write: %*ph\n",
+ 7, shark->transfer_buffer);
+
+ res = usb_interrupt_msg(shark->usbdev,
+ usb_sndintpipe(shark->usbdev, SHARK_OUT_EP),
+ shark->transfer_buffer, TB_LEN,
+ &actual_len, 1000);
+ if (res < 0) {
+ v4l2_err(tea->v4l2_dev, "write error: %d\n", res);
+ return res;
+ }
+
+ return 0;
+}
+
+static int shark_read_reg(struct radio_tea5777 *tea, u32 *reg_ret)
+{
+ struct shark_device *shark = tea->private_data;
+ int i, res, actual_len;
+ u32 reg = 0;
+
+ memset(shark->transfer_buffer, 0, TB_LEN);
+ shark->transfer_buffer[0] = 0x82;
+ res = usb_interrupt_msg(shark->usbdev,
+ usb_sndintpipe(shark->usbdev, SHARK_OUT_EP),
+ shark->transfer_buffer, TB_LEN,
+ &actual_len, 1000);
+ if (res < 0) {
+ v4l2_err(tea->v4l2_dev, "request-read error: %d\n", res);
+ return res;
+ }
+
+ res = usb_interrupt_msg(shark->usbdev,
+ usb_rcvintpipe(shark->usbdev, SHARK_IN_EP),
+ shark->transfer_buffer, TB_LEN,
+ &actual_len, 1000);
+ if (res < 0) {
+ v4l2_err(tea->v4l2_dev, "read error: %d\n", res);
+ return res;
+ }
+
+ for (i = 0; i < 3; i++)
+ reg |= shark->transfer_buffer[i] << (16 - i * 8);
+
+ v4l2_dbg(1, debug, tea->v4l2_dev, "shark2-read: %*ph\n",
+ 3, shark->transfer_buffer);
+
+ *reg_ret = reg;
+ return 0;
+}
+
+static const struct radio_tea5777_ops shark_tea_ops = {
+ .write_reg = shark_write_reg,
+ .read_reg = shark_read_reg,
+};
+
+#ifdef SHARK_USE_LEDS
+static void shark_led_work(struct work_struct *work)
+{
+ struct shark_device *shark =
+ container_of(work, struct shark_device, led_work);
+ int i, res, brightness, actual_len;
+
+ for (i = 0; i < 2; i++) {
+ if (!test_and_clear_bit(i, &shark->brightness_new))
+ continue;
+
+ brightness = atomic_read(&shark->brightness[i]);
+ memset(shark->transfer_buffer, 0, TB_LEN);
+ shark->transfer_buffer[0] = 0x83 + i;
+ shark->transfer_buffer[1] = brightness;
+ res = usb_interrupt_msg(shark->usbdev,
+ usb_sndintpipe(shark->usbdev,
+ SHARK_OUT_EP),
+ shark->transfer_buffer, TB_LEN,
+ &actual_len, 1000);
+ if (res < 0)
+ v4l2_err(&shark->v4l2_dev, "set LED %s error: %d\n",
+ shark->led_names[i], res);
+ }
+}
+
+static void shark_led_set_blue(struct led_classdev *led_cdev,
+ enum led_brightness value)
+{
+ struct shark_device *shark =
+ container_of(led_cdev, struct shark_device, leds[BLUE_LED]);
+
+ atomic_set(&shark->brightness[BLUE_LED], value);
+ set_bit(BLUE_LED, &shark->brightness_new);
+ schedule_work(&shark->led_work);
+}
+
+static void shark_led_set_red(struct led_classdev *led_cdev,
+ enum led_brightness value)
+{
+ struct shark_device *shark =
+ container_of(led_cdev, struct shark_device, leds[RED_LED]);
+
+ atomic_set(&shark->brightness[RED_LED], value);
+ set_bit(RED_LED, &shark->brightness_new);
+ schedule_work(&shark->led_work);
+}
+
+static const struct led_classdev shark_led_templates[NO_LEDS] = {
+ [BLUE_LED] = {
+ .name = "%s:blue:",
+ .brightness = LED_OFF,
+ .max_brightness = 127,
+ .brightness_set = shark_led_set_blue,
+ },
+ [RED_LED] = {
+ .name = "%s:red:",
+ .brightness = LED_OFF,
+ .max_brightness = 1,
+ .brightness_set = shark_led_set_red,
+ },
+};
+
+static int shark_register_leds(struct shark_device *shark, struct device *dev)
+{
+ int i, retval;
+
+ atomic_set(&shark->brightness[BLUE_LED], 127);
+ INIT_WORK(&shark->led_work, shark_led_work);
+ for (i = 0; i < NO_LEDS; i++) {
+ shark->leds[i] = shark_led_templates[i];
+ snprintf(shark->led_names[i], sizeof(shark->led_names[0]),
+ shark->leds[i].name, shark->v4l2_dev.name);
+ shark->leds[i].name = shark->led_names[i];
+ retval = led_classdev_register(dev, &shark->leds[i]);
+ if (retval) {
+ v4l2_err(&shark->v4l2_dev,
+ "couldn't register led: %s\n",
+ shark->led_names[i]);
+ return retval;
+ }
+ }
+ return 0;
+}
+
+static void shark_unregister_leds(struct shark_device *shark)
+{
+ int i;
+
+ for (i = 0; i < NO_LEDS; i++)
+ led_classdev_unregister(&shark->leds[i]);
+
+ cancel_work_sync(&shark->led_work);
+}
+
+static inline void shark_resume_leds(struct shark_device *shark)
+{
+ int i;
+
+ for (i = 0; i < NO_LEDS; i++)
+ set_bit(i, &shark->brightness_new);
+
+ schedule_work(&shark->led_work);
+}
+#else
+static int shark_register_leds(struct shark_device *shark, struct device *dev)
+{
+ v4l2_warn(&shark->v4l2_dev,
+ "CONFIG_LEDS_CLASS not enabled, LED support disabled\n");
+ return 0;
+}
+static inline void shark_unregister_leds(struct shark_device *shark) { }
+static inline void shark_resume_leds(struct shark_device *shark) { }
+#endif
+
+static void usb_shark_disconnect(struct usb_interface *intf)
+{
+ struct v4l2_device *v4l2_dev = usb_get_intfdata(intf);
+ struct shark_device *shark = v4l2_dev_to_shark(v4l2_dev);
+
+ mutex_lock(&shark->tea.mutex);
+ v4l2_device_disconnect(&shark->v4l2_dev);
+ radio_tea5777_exit(&shark->tea);
+ mutex_unlock(&shark->tea.mutex);
+
+ shark_unregister_leds(shark);
+
+ v4l2_device_put(&shark->v4l2_dev);
+}
+
+static void usb_shark_release(struct v4l2_device *v4l2_dev)
+{
+ struct shark_device *shark = v4l2_dev_to_shark(v4l2_dev);
+
+ v4l2_device_unregister(&shark->v4l2_dev);
+ kfree(shark->transfer_buffer);
+ kfree(shark);
+}
+
+static int usb_shark_probe(struct usb_interface *intf,
+ const struct usb_device_id *id)
+{
+ struct shark_device *shark;
+ int retval = -ENOMEM;
+ static const u8 ep_addresses[] = {
+ SHARK_IN_EP | USB_DIR_IN,
+ SHARK_OUT_EP | USB_DIR_OUT,
+ 0};
+
+ /* Are the expected endpoints present? */
+ if (!usb_check_int_endpoints(intf, ep_addresses)) {
+ dev_err(&intf->dev, "Invalid radioSHARK2 device\n");
+ return -EINVAL;
+ }
+
+ shark = kzalloc_obj(struct shark_device);
+ if (!shark)
+ return retval;
+
+ shark->transfer_buffer = kmalloc(TB_LEN, GFP_KERNEL);
+ if (!shark->transfer_buffer)
+ goto err_alloc_buffer;
+
+ v4l2_device_set_name(&shark->v4l2_dev, DRV_NAME, &shark_instance);
+
+ retval = shark_register_leds(shark, &intf->dev);
+ if (retval)
+ goto err_reg_leds;
+
+ shark->v4l2_dev.release = usb_shark_release;
+ retval = v4l2_device_register(&intf->dev, &shark->v4l2_dev);
+ if (retval) {
+ v4l2_err(&shark->v4l2_dev, "couldn't register v4l2_device\n");
+ goto err_reg_dev;
+ }
+
+ shark->usbdev = interface_to_usbdev(intf);
+ shark->tea.v4l2_dev = &shark->v4l2_dev;
+ shark->tea.private_data = shark;
+ shark->tea.ops = &shark_tea_ops;
+ shark->tea.has_am = true;
+ shark->tea.write_before_read = true;
+ strscpy(shark->tea.card, "Griffin radioSHARK2",
+ sizeof(shark->tea.card));
+ usb_make_path(shark->usbdev, shark->tea.bus_info,
+ sizeof(shark->tea.bus_info));
+
+ retval = radio_tea5777_init(&shark->tea, THIS_MODULE);
+ if (retval) {
+ v4l2_err(&shark->v4l2_dev, "couldn't init tea5777\n");
+ goto err_init_tea;
+ }
+
+ return 0;
+
+err_init_tea:
+ v4l2_device_unregister(&shark->v4l2_dev);
+err_reg_dev:
+ shark_unregister_leds(shark);
+err_reg_leds:
+ kfree(shark->transfer_buffer);
+err_alloc_buffer:
+ kfree(shark);
+
+ return retval;
+}
+
+#ifdef CONFIG_PM
+static int usb_shark_suspend(struct usb_interface *intf, pm_message_t message)
+{
+ return 0;
+}
+
+static int usb_shark_resume(struct usb_interface *intf)
+{
+ struct v4l2_device *v4l2_dev = usb_get_intfdata(intf);
+ struct shark_device *shark = v4l2_dev_to_shark(v4l2_dev);
+ int ret;
+
+ mutex_lock(&shark->tea.mutex);
+ ret = radio_tea5777_set_freq(&shark->tea);
+ mutex_unlock(&shark->tea.mutex);
+
+ shark_resume_leds(shark);
+
+ return ret;
+}
+#endif
+
+/* Specify the bcdDevice value, as the radioSHARK and radioSHARK2 share ids */
+static const struct usb_device_id usb_shark_device_table[] = {
+ { .match_flags = USB_DEVICE_ID_MATCH_DEVICE_AND_VERSION |
+ USB_DEVICE_ID_MATCH_INT_CLASS,
+ .idVendor = 0x077d,
+ .idProduct = 0x627a,
+ .bcdDevice_lo = 0x0010,
+ .bcdDevice_hi = 0x0010,
+ .bInterfaceClass = 3,
+ },
+ { }
+};
+MODULE_DEVICE_TABLE(usb, usb_shark_device_table);
+
+static struct usb_driver usb_shark_driver = {
+ .name = DRV_NAME,
+ .probe = usb_shark_probe,
+ .disconnect = usb_shark_disconnect,
+ .id_table = usb_shark_device_table,
+#ifdef CONFIG_PM
+ .suspend = usb_shark_suspend,
+ .resume = usb_shark_resume,
+ .reset_resume = usb_shark_resume,
+#endif
+};
+module_usb_driver(usb_shark_driver);
diff --git a/drivers/media/radio/radio-si476x.c b/drivers/media/radio/radio-si476x.c
new file mode 100644
index 000000000..bfe89782d
--- /dev/null
+++ b/drivers/media/radio/radio-si476x.c
@@ -0,0 +1,1523 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * drivers/media/radio/radio-si476x.c -- V4L2 driver for SI476X chips
+ *
+ * Copyright (C) 2012 Innovative Converged Devices(ICD)
+ * Copyright (C) 2013 Andrey Smirnov
+ *
+ * Author: Andrey Smirnov <andrew.smirnov@gmail.com>
+ */
+
+#include <linux/module.h>
+#include <linux/delay.h>
+#include <linux/interrupt.h>
+#include <linux/slab.h>
+#include <linux/atomic.h>
+#include <linux/videodev2.h>
+#include <linux/mutex.h>
+#include <linux/debugfs.h>
+#include <media/v4l2-common.h>
+#include <media/v4l2-ioctl.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-event.h>
+#include <media/v4l2-device.h>
+
+#include <media/drv-intf/si476x.h>
+#include <linux/mfd/si476x-core.h>
+
+#define FM_FREQ_RANGE_LOW 64000000
+#define FM_FREQ_RANGE_HIGH 108000000
+
+#define AM_FREQ_RANGE_LOW 520000
+#define AM_FREQ_RANGE_HIGH 30000000
+
+#define PWRLINEFLTR (1 << 8)
+
+#define FREQ_MUL (10000000 / 625)
+
+#define SI476X_PHDIV_STATUS_LINK_LOCKED(status) (0x80 & (status))
+
+#define DRIVER_NAME "si476x-radio"
+#define DRIVER_CARD "SI476x AM/FM Receiver"
+
+enum si476x_freq_bands {
+ SI476X_BAND_FM,
+ SI476X_BAND_AM,
+};
+
+static const struct v4l2_frequency_band si476x_bands[] = {
+ [SI476X_BAND_FM] = {
+ .type = V4L2_TUNER_RADIO,
+ .index = SI476X_BAND_FM,
+ .capability = V4L2_TUNER_CAP_LOW
+ | V4L2_TUNER_CAP_STEREO
+ | V4L2_TUNER_CAP_RDS
+ | V4L2_TUNER_CAP_RDS_BLOCK_IO
+ | V4L2_TUNER_CAP_FREQ_BANDS,
+ .rangelow = 64 * FREQ_MUL,
+ .rangehigh = 108 * FREQ_MUL,
+ .modulation = V4L2_BAND_MODULATION_FM,
+ },
+ [SI476X_BAND_AM] = {
+ .type = V4L2_TUNER_RADIO,
+ .index = SI476X_BAND_AM,
+ .capability = V4L2_TUNER_CAP_LOW
+ | V4L2_TUNER_CAP_FREQ_BANDS,
+ .rangelow = 0.52 * FREQ_MUL,
+ .rangehigh = 30 * FREQ_MUL,
+ .modulation = V4L2_BAND_MODULATION_AM,
+ },
+};
+
+static inline bool si476x_radio_freq_is_inside_of_the_band(u32 freq, int band)
+{
+ return freq >= si476x_bands[band].rangelow &&
+ freq <= si476x_bands[band].rangehigh;
+}
+
+static inline bool si476x_radio_range_is_inside_of_the_band(u32 low, u32 high,
+ int band)
+{
+ return low >= si476x_bands[band].rangelow &&
+ high <= si476x_bands[band].rangehigh;
+}
+
+static int si476x_radio_s_ctrl(struct v4l2_ctrl *ctrl);
+static int si476x_radio_g_volatile_ctrl(struct v4l2_ctrl *ctrl);
+
+enum phase_diversity_modes_idx {
+ SI476X_IDX_PHDIV_DISABLED,
+ SI476X_IDX_PHDIV_PRIMARY_COMBINING,
+ SI476X_IDX_PHDIV_PRIMARY_ANTENNA,
+ SI476X_IDX_PHDIV_SECONDARY_ANTENNA,
+ SI476X_IDX_PHDIV_SECONDARY_COMBINING,
+};
+
+static const char * const phase_diversity_modes[] = {
+ [SI476X_IDX_PHDIV_DISABLED] = "Disabled",
+ [SI476X_IDX_PHDIV_PRIMARY_COMBINING] = "Primary with Secondary",
+ [SI476X_IDX_PHDIV_PRIMARY_ANTENNA] = "Primary Antenna",
+ [SI476X_IDX_PHDIV_SECONDARY_ANTENNA] = "Secondary Antenna",
+ [SI476X_IDX_PHDIV_SECONDARY_COMBINING] = "Secondary with Primary",
+};
+
+static inline enum phase_diversity_modes_idx
+si476x_phase_diversity_mode_to_idx(enum si476x_phase_diversity_mode mode)
+{
+ switch (mode) {
+ default:
+ fallthrough;
+ case SI476X_PHDIV_DISABLED:
+ return SI476X_IDX_PHDIV_DISABLED;
+ case SI476X_PHDIV_PRIMARY_COMBINING:
+ return SI476X_IDX_PHDIV_PRIMARY_COMBINING;
+ case SI476X_PHDIV_PRIMARY_ANTENNA:
+ return SI476X_IDX_PHDIV_PRIMARY_ANTENNA;
+ case SI476X_PHDIV_SECONDARY_ANTENNA:
+ return SI476X_IDX_PHDIV_SECONDARY_ANTENNA;
+ case SI476X_PHDIV_SECONDARY_COMBINING:
+ return SI476X_IDX_PHDIV_SECONDARY_COMBINING;
+ }
+}
+
+static inline enum si476x_phase_diversity_mode
+si476x_phase_diversity_idx_to_mode(enum phase_diversity_modes_idx idx)
+{
+ static const int idx_to_value[] = {
+ [SI476X_IDX_PHDIV_DISABLED] = SI476X_PHDIV_DISABLED,
+ [SI476X_IDX_PHDIV_PRIMARY_COMBINING] = SI476X_PHDIV_PRIMARY_COMBINING,
+ [SI476X_IDX_PHDIV_PRIMARY_ANTENNA] = SI476X_PHDIV_PRIMARY_ANTENNA,
+ [SI476X_IDX_PHDIV_SECONDARY_ANTENNA] = SI476X_PHDIV_SECONDARY_ANTENNA,
+ [SI476X_IDX_PHDIV_SECONDARY_COMBINING] = SI476X_PHDIV_SECONDARY_COMBINING,
+ };
+
+ return idx_to_value[idx];
+}
+
+static const struct v4l2_ctrl_ops si476x_ctrl_ops = {
+ .g_volatile_ctrl = si476x_radio_g_volatile_ctrl,
+ .s_ctrl = si476x_radio_s_ctrl,
+};
+
+
+enum si476x_ctrl_idx {
+ SI476X_IDX_RSSI_THRESHOLD,
+ SI476X_IDX_SNR_THRESHOLD,
+ SI476X_IDX_MAX_TUNE_ERROR,
+ SI476X_IDX_HARMONICS_COUNT,
+ SI476X_IDX_DIVERSITY_MODE,
+ SI476X_IDX_INTERCHIP_LINK,
+};
+static struct v4l2_ctrl_config si476x_ctrls[] = {
+
+ /*
+ * SI476X during its station seeking(or tuning) process uses several
+ * parameters to determine if "the station" is valid:
+ *
+ * - Signal's SNR(in dBuV) must be lower than
+ * #V4L2_CID_SI476X_SNR_THRESHOLD
+ * - Signal's RSSI(in dBuV) must be greater than
+ * #V4L2_CID_SI476X_RSSI_THRESHOLD
+ * - Signal's frequency deviation(in units of 2ppm) must not be
+ * more than #V4L2_CID_SI476X_MAX_TUNE_ERROR
+ */
+ [SI476X_IDX_RSSI_THRESHOLD] = {
+ .ops = &si476x_ctrl_ops,
+ .id = V4L2_CID_SI476X_RSSI_THRESHOLD,
+ .name = "Valid RSSI Threshold",
+ .type = V4L2_CTRL_TYPE_INTEGER,
+ .min = -128,
+ .max = 127,
+ .step = 1,
+ },
+ [SI476X_IDX_SNR_THRESHOLD] = {
+ .ops = &si476x_ctrl_ops,
+ .id = V4L2_CID_SI476X_SNR_THRESHOLD,
+ .type = V4L2_CTRL_TYPE_INTEGER,
+ .name = "Valid SNR Threshold",
+ .min = -128,
+ .max = 127,
+ .step = 1,
+ },
+ [SI476X_IDX_MAX_TUNE_ERROR] = {
+ .ops = &si476x_ctrl_ops,
+ .id = V4L2_CID_SI476X_MAX_TUNE_ERROR,
+ .type = V4L2_CTRL_TYPE_INTEGER,
+ .name = "Max Tune Errors",
+ .min = 0,
+ .max = 126 * 2,
+ .step = 2,
+ },
+
+ /*
+ * #V4L2_CID_SI476X_HARMONICS_COUNT -- number of harmonics
+ * built-in power-line noise supression filter is to reject
+ * during AM-mode operation.
+ */
+ [SI476X_IDX_HARMONICS_COUNT] = {
+ .ops = &si476x_ctrl_ops,
+ .id = V4L2_CID_SI476X_HARMONICS_COUNT,
+ .type = V4L2_CTRL_TYPE_INTEGER,
+
+ .name = "Count of Harmonics to Reject",
+ .min = 0,
+ .max = 20,
+ .step = 1,
+ },
+
+ /*
+ * #V4L2_CID_SI476X_DIVERSITY_MODE -- configuration which
+ * two tuners working in diversity mode are to work in.
+ *
+ * - #SI476X_IDX_PHDIV_DISABLED diversity mode disabled
+ * - #SI476X_IDX_PHDIV_PRIMARY_COMBINING diversity mode is
+ * on, primary tuner's antenna is the main one.
+ * - #SI476X_IDX_PHDIV_PRIMARY_ANTENNA diversity mode is
+ * off, primary tuner's antenna is the main one.
+ * - #SI476X_IDX_PHDIV_SECONDARY_ANTENNA diversity mode is
+ * off, secondary tuner's antenna is the main one.
+ * - #SI476X_IDX_PHDIV_SECONDARY_COMBINING diversity mode is
+ * on, secondary tuner's antenna is the main one.
+ */
+ [SI476X_IDX_DIVERSITY_MODE] = {
+ .ops = &si476x_ctrl_ops,
+ .id = V4L2_CID_SI476X_DIVERSITY_MODE,
+ .type = V4L2_CTRL_TYPE_MENU,
+ .name = "Phase Diversity Mode",
+ .qmenu = phase_diversity_modes,
+ .min = 0,
+ .max = ARRAY_SIZE(phase_diversity_modes) - 1,
+ },
+
+ /*
+ * #V4L2_CID_SI476X_INTERCHIP_LINK -- inter-chip link in
+ * diversity mode indicator. Allows user to determine if two
+ * chips working in diversity mode have established a link
+ * between each other and if the system as a whole uses
+ * signals from both antennas to receive FM radio.
+ */
+ [SI476X_IDX_INTERCHIP_LINK] = {
+ .ops = &si476x_ctrl_ops,
+ .id = V4L2_CID_SI476X_INTERCHIP_LINK,
+ .type = V4L2_CTRL_TYPE_BOOLEAN,
+ .flags = V4L2_CTRL_FLAG_READ_ONLY | V4L2_CTRL_FLAG_VOLATILE,
+ .name = "Inter-Chip Link",
+ .min = 0,
+ .max = 1,
+ .step = 1,
+ },
+};
+
+struct si476x_radio;
+
+/**
+ * struct si476x_radio_ops - vtable of tuner functions
+ *
+ * This table holds pointers to functions implementing particular
+ * operations depending on the mode in which the tuner chip was
+ * configured to start. If the function is not supported
+ * corresponding element is set to #NULL.
+ *
+ * @tune_freq: Tune chip to a specific frequency
+ * @seek_start: Star station seeking
+ * @rsq_status: Get Received Signal Quality(RSQ) status
+ * @rds_blckcnt: Get received RDS blocks count
+ * @phase_diversity: Change phase diversity mode of the tuner
+ * @phase_div_status: Get phase diversity mode status
+ * @acf_status: Get the status of Automatically Controlled
+ * Features(ACF)
+ * @agc_status: Get Automatic Gain Control(AGC) status
+ */
+struct si476x_radio_ops {
+ int (*tune_freq)(struct si476x_core *, struct si476x_tune_freq_args *);
+ int (*seek_start)(struct si476x_core *, bool, bool);
+ int (*rsq_status)(struct si476x_core *, struct si476x_rsq_status_args *,
+ struct si476x_rsq_status_report *);
+ int (*rds_blckcnt)(struct si476x_core *, bool,
+ struct si476x_rds_blockcount_report *);
+
+ int (*phase_diversity)(struct si476x_core *,
+ enum si476x_phase_diversity_mode);
+ int (*phase_div_status)(struct si476x_core *);
+ int (*acf_status)(struct si476x_core *,
+ struct si476x_acf_status_report *);
+ int (*agc_status)(struct si476x_core *,
+ struct si476x_agc_status_report *);
+};
+
+/**
+ * struct si476x_radio - radio device
+ *
+ * @v4l2dev: Pointer to V4L2 device created by V4L2 subsystem
+ * @videodev: Pointer to video device created by V4L2 subsystem
+ * @ctrl_handler: V4L2 controls handler
+ * @core: Pointer to underlying core device
+ * @ops: Vtable of functions. See struct si476x_radio_ops for details
+ * @debugfs: pointer to &strucd dentry for debugfs
+ * @audmode: audio mode, as defined for the rxsubchans field
+ * at videodev2.h
+ *
+ * core structure is the radio device is being used
+ */
+struct si476x_radio {
+ struct v4l2_device v4l2dev;
+ struct video_device videodev;
+ struct v4l2_ctrl_handler ctrl_handler;
+
+ struct si476x_core *core;
+ /* This field should not be accesses unless core lock is held */
+ const struct si476x_radio_ops *ops;
+
+ struct dentry *debugfs;
+ u32 audmode;
+};
+
+static inline struct si476x_radio *
+v4l2_ctrl_handler_to_radio(struct v4l2_ctrl_handler *d)
+{
+ return container_of(d, struct si476x_radio, ctrl_handler);
+}
+
+/*
+ * si476x_vidioc_querycap - query device capabilities
+ */
+static int si476x_radio_querycap(struct file *file, void *priv,
+ struct v4l2_capability *capability)
+{
+ struct si476x_radio *radio = video_drvdata(file);
+
+ strscpy(capability->driver, radio->v4l2dev.name,
+ sizeof(capability->driver));
+ strscpy(capability->card, DRIVER_CARD, sizeof(capability->card));
+ return 0;
+}
+
+static int si476x_radio_enum_freq_bands(struct file *file, void *priv,
+ struct v4l2_frequency_band *band)
+{
+ int err;
+ struct si476x_radio *radio = video_drvdata(file);
+
+ if (band->tuner != 0)
+ return -EINVAL;
+
+ switch (radio->core->chip_id) {
+ /* AM/FM tuners -- all bands are supported */
+ case SI476X_CHIP_SI4761:
+ case SI476X_CHIP_SI4764:
+ if (band->index < ARRAY_SIZE(si476x_bands)) {
+ *band = si476x_bands[band->index];
+ err = 0;
+ } else {
+ err = -EINVAL;
+ }
+ break;
+ /* FM companion tuner chips -- only FM bands are
+ * supported */
+ case SI476X_CHIP_SI4768:
+ if (band->index == SI476X_BAND_FM) {
+ *band = si476x_bands[band->index];
+ err = 0;
+ } else {
+ err = -EINVAL;
+ }
+ break;
+ default:
+ err = -EINVAL;
+ }
+
+ return err;
+}
+
+static int si476x_radio_g_tuner(struct file *file, void *priv,
+ struct v4l2_tuner *tuner)
+{
+ int err;
+ struct si476x_rsq_status_report report;
+ struct si476x_radio *radio = video_drvdata(file);
+
+ struct si476x_rsq_status_args args = {
+ .primary = false,
+ .rsqack = false,
+ .attune = false,
+ .cancel = false,
+ .stcack = false,
+ };
+
+ if (tuner->index != 0)
+ return -EINVAL;
+
+ tuner->type = V4L2_TUNER_RADIO;
+ tuner->capability = V4L2_TUNER_CAP_LOW /* Measure frequencies
+ * in multiples of
+ * 62.5 Hz */
+ | V4L2_TUNER_CAP_STEREO
+ | V4L2_TUNER_CAP_HWSEEK_BOUNDED
+ | V4L2_TUNER_CAP_HWSEEK_WRAP
+ | V4L2_TUNER_CAP_HWSEEK_PROG_LIM;
+
+ si476x_core_lock(radio->core);
+
+ if (si476x_core_is_a_secondary_tuner(radio->core)) {
+ strscpy(tuner->name, "FM (secondary)", sizeof(tuner->name));
+ tuner->rxsubchans = 0;
+ tuner->rangelow = si476x_bands[SI476X_BAND_FM].rangelow;
+ } else if (si476x_core_has_am(radio->core)) {
+ if (si476x_core_is_a_primary_tuner(radio->core))
+ strscpy(tuner->name, "AM/FM (primary)",
+ sizeof(tuner->name));
+ else
+ strscpy(tuner->name, "AM/FM", sizeof(tuner->name));
+
+ tuner->rxsubchans = V4L2_TUNER_SUB_MONO | V4L2_TUNER_SUB_STEREO
+ | V4L2_TUNER_SUB_RDS;
+ tuner->capability |= V4L2_TUNER_CAP_RDS
+ | V4L2_TUNER_CAP_RDS_BLOCK_IO
+ | V4L2_TUNER_CAP_FREQ_BANDS;
+
+ tuner->rangelow = si476x_bands[SI476X_BAND_AM].rangelow;
+ } else {
+ strscpy(tuner->name, "FM", sizeof(tuner->name));
+ tuner->rxsubchans = V4L2_TUNER_SUB_RDS;
+ tuner->capability |= V4L2_TUNER_CAP_RDS
+ | V4L2_TUNER_CAP_RDS_BLOCK_IO
+ | V4L2_TUNER_CAP_FREQ_BANDS;
+ tuner->rangelow = si476x_bands[SI476X_BAND_FM].rangelow;
+ }
+
+ tuner->audmode = radio->audmode;
+
+ tuner->afc = 1;
+ tuner->rangehigh = si476x_bands[SI476X_BAND_FM].rangehigh;
+
+ err = radio->ops->rsq_status(radio->core,
+ &args, &report);
+ if (err < 0) {
+ tuner->signal = 0;
+ } else {
+ /*
+ * tuner->signal value range: 0x0000 .. 0xFFFF,
+ * report.rssi: -128 .. 127
+ */
+ tuner->signal = (report.rssi + 128) * 257;
+ }
+ si476x_core_unlock(radio->core);
+
+ return err;
+}
+
+static int si476x_radio_s_tuner(struct file *file, void *priv,
+ const struct v4l2_tuner *tuner)
+{
+ struct si476x_radio *radio = video_drvdata(file);
+
+ if (tuner->index != 0)
+ return -EINVAL;
+
+ if (tuner->audmode == V4L2_TUNER_MODE_MONO ||
+ tuner->audmode == V4L2_TUNER_MODE_STEREO)
+ radio->audmode = tuner->audmode;
+ else
+ radio->audmode = V4L2_TUNER_MODE_STEREO;
+
+ return 0;
+}
+
+static int si476x_radio_init_vtable(struct si476x_radio *radio,
+ enum si476x_func func)
+{
+ static const struct si476x_radio_ops fm_ops = {
+ .tune_freq = si476x_core_cmd_fm_tune_freq,
+ .seek_start = si476x_core_cmd_fm_seek_start,
+ .rsq_status = si476x_core_cmd_fm_rsq_status,
+ .rds_blckcnt = si476x_core_cmd_fm_rds_blockcount,
+ .phase_diversity = si476x_core_cmd_fm_phase_diversity,
+ .phase_div_status = si476x_core_cmd_fm_phase_div_status,
+ .acf_status = si476x_core_cmd_fm_acf_status,
+ .agc_status = si476x_core_cmd_agc_status,
+ };
+
+ static const struct si476x_radio_ops am_ops = {
+ .tune_freq = si476x_core_cmd_am_tune_freq,
+ .seek_start = si476x_core_cmd_am_seek_start,
+ .rsq_status = si476x_core_cmd_am_rsq_status,
+ .rds_blckcnt = NULL,
+ .phase_diversity = NULL,
+ .phase_div_status = NULL,
+ .acf_status = si476x_core_cmd_am_acf_status,
+ .agc_status = NULL,
+ };
+
+ switch (func) {
+ case SI476X_FUNC_FM_RECEIVER:
+ radio->ops = &fm_ops;
+ return 0;
+
+ case SI476X_FUNC_AM_RECEIVER:
+ radio->ops = &am_ops;
+ return 0;
+ default:
+ WARN(1, "Unexpected tuner function value\n");
+ return -EINVAL;
+ }
+}
+
+static int si476x_radio_pretune(struct si476x_radio *radio,
+ enum si476x_func func)
+{
+ int retval;
+
+ struct si476x_tune_freq_args args = {
+ .zifsr = false,
+ .hd = false,
+ .injside = SI476X_INJSIDE_AUTO,
+ .tunemode = SI476X_TM_VALIDATED_NORMAL_TUNE,
+ .smoothmetrics = SI476X_SM_INITIALIZE_AUDIO,
+ .antcap = 0,
+ };
+
+ switch (func) {
+ case SI476X_FUNC_FM_RECEIVER:
+ args.freq = v4l2_to_si476x(radio->core,
+ 92 * FREQ_MUL);
+ retval = radio->ops->tune_freq(radio->core, &args);
+ break;
+ case SI476X_FUNC_AM_RECEIVER:
+ args.freq = v4l2_to_si476x(radio->core,
+ 0.6 * FREQ_MUL);
+ retval = radio->ops->tune_freq(radio->core, &args);
+ break;
+ default:
+ WARN(1, "Unexpected tuner function value\n");
+ retval = -EINVAL;
+ }
+
+ return retval;
+}
+static int si476x_radio_do_post_powerup_init(struct si476x_radio *radio,
+ enum si476x_func func)
+{
+ int err;
+
+ /* regcache_mark_dirty(radio->core->regmap); */
+ err = regcache_sync_region(radio->core->regmap,
+ SI476X_PROP_DIGITAL_IO_INPUT_SAMPLE_RATE,
+ SI476X_PROP_DIGITAL_IO_OUTPUT_FORMAT);
+ if (err < 0)
+ return err;
+
+ err = regcache_sync_region(radio->core->regmap,
+ SI476X_PROP_AUDIO_DEEMPHASIS,
+ SI476X_PROP_AUDIO_PWR_LINE_FILTER);
+ if (err < 0)
+ return err;
+
+ err = regcache_sync_region(radio->core->regmap,
+ SI476X_PROP_INT_CTL_ENABLE,
+ SI476X_PROP_INT_CTL_ENABLE);
+ if (err < 0)
+ return err;
+
+ /*
+ * Is there any point in restoring SNR and the like
+ * when switching between AM/FM?
+ */
+ err = regcache_sync_region(radio->core->regmap,
+ SI476X_PROP_VALID_MAX_TUNE_ERROR,
+ SI476X_PROP_VALID_MAX_TUNE_ERROR);
+ if (err < 0)
+ return err;
+
+ err = regcache_sync_region(radio->core->regmap,
+ SI476X_PROP_VALID_SNR_THRESHOLD,
+ SI476X_PROP_VALID_RSSI_THRESHOLD);
+ if (err < 0)
+ return err;
+
+ if (func == SI476X_FUNC_FM_RECEIVER) {
+ if (si476x_core_has_diversity(radio->core)) {
+ err = si476x_core_cmd_fm_phase_diversity(radio->core,
+ radio->core->diversity_mode);
+ if (err < 0)
+ return err;
+ }
+
+ err = regcache_sync_region(radio->core->regmap,
+ SI476X_PROP_FM_RDS_INTERRUPT_SOURCE,
+ SI476X_PROP_FM_RDS_CONFIG);
+ if (err < 0)
+ return err;
+ }
+
+ return si476x_radio_init_vtable(radio, func);
+
+}
+
+static int si476x_radio_change_func(struct si476x_radio *radio,
+ enum si476x_func func)
+{
+ int err;
+ bool soft;
+ /*
+ * Since power/up down is a very time consuming operation,
+ * try to avoid doing it if the requested mode matches the one
+ * the tuner is in
+ */
+ if (func == radio->core->power_up_parameters.func)
+ return 0;
+
+ soft = true;
+ err = si476x_core_stop(radio->core, soft);
+ if (err < 0) {
+ /*
+ * OK, if the chip does not want to play nice let's
+ * try to reset it in more brutal way
+ */
+ soft = false;
+ err = si476x_core_stop(radio->core, soft);
+ if (err < 0)
+ return err;
+ }
+ /*
+ Set the desired radio tuner function
+ */
+ radio->core->power_up_parameters.func = func;
+
+ err = si476x_core_start(radio->core, soft);
+ if (err < 0)
+ return err;
+
+ /*
+ * No need to do the rest of manipulations for the bootlader
+ * mode
+ */
+ if (func != SI476X_FUNC_FM_RECEIVER &&
+ func != SI476X_FUNC_AM_RECEIVER)
+ return err;
+
+ return si476x_radio_do_post_powerup_init(radio, func);
+}
+
+static int si476x_radio_g_frequency(struct file *file, void *priv,
+ struct v4l2_frequency *f)
+{
+ int err;
+ struct si476x_radio *radio = video_drvdata(file);
+
+ if (f->tuner != 0 ||
+ f->type != V4L2_TUNER_RADIO)
+ return -EINVAL;
+
+ si476x_core_lock(radio->core);
+
+ if (radio->ops->rsq_status) {
+ struct si476x_rsq_status_report report;
+ struct si476x_rsq_status_args args = {
+ .primary = false,
+ .rsqack = false,
+ .attune = true,
+ .cancel = false,
+ .stcack = false,
+ };
+
+ err = radio->ops->rsq_status(radio->core, &args, &report);
+ if (!err)
+ f->frequency = si476x_to_v4l2(radio->core,
+ report.readfreq);
+ } else {
+ err = -EINVAL;
+ }
+
+ si476x_core_unlock(radio->core);
+
+ return err;
+}
+
+static int si476x_radio_s_frequency(struct file *file, void *priv,
+ const struct v4l2_frequency *f)
+{
+ int err;
+ u32 freq = f->frequency;
+ struct si476x_tune_freq_args args;
+ struct si476x_radio *radio = video_drvdata(file);
+
+ const u32 midrange = (si476x_bands[SI476X_BAND_AM].rangehigh +
+ si476x_bands[SI476X_BAND_FM].rangelow) / 2;
+ const int band = (freq > midrange) ?
+ SI476X_BAND_FM : SI476X_BAND_AM;
+ const enum si476x_func func = (band == SI476X_BAND_AM) ?
+ SI476X_FUNC_AM_RECEIVER : SI476X_FUNC_FM_RECEIVER;
+
+ if (f->tuner != 0 ||
+ f->type != V4L2_TUNER_RADIO)
+ return -EINVAL;
+
+ si476x_core_lock(radio->core);
+
+ freq = clamp(freq,
+ si476x_bands[band].rangelow,
+ si476x_bands[band].rangehigh);
+
+ if (si476x_radio_freq_is_inside_of_the_band(freq,
+ SI476X_BAND_AM) &&
+ (!si476x_core_has_am(radio->core) ||
+ si476x_core_is_a_secondary_tuner(radio->core))) {
+ err = -EINVAL;
+ goto unlock;
+ }
+
+ err = si476x_radio_change_func(radio, func);
+ if (err < 0)
+ goto unlock;
+
+ args.zifsr = false;
+ args.hd = false;
+ args.injside = SI476X_INJSIDE_AUTO;
+ args.freq = v4l2_to_si476x(radio->core, freq);
+ args.tunemode = SI476X_TM_VALIDATED_NORMAL_TUNE;
+ args.smoothmetrics = SI476X_SM_INITIALIZE_AUDIO;
+ args.antcap = 0;
+
+ err = radio->ops->tune_freq(radio->core, &args);
+
+unlock:
+ si476x_core_unlock(radio->core);
+ return err;
+}
+
+static int si476x_radio_s_hw_freq_seek(struct file *file, void *priv,
+ const struct v4l2_hw_freq_seek *seek)
+{
+ int err;
+ enum si476x_func func;
+ u32 rangelow = seek->rangelow, rangehigh = seek->rangehigh;
+ struct si476x_radio *radio = video_drvdata(file);
+
+ if (file->f_flags & O_NONBLOCK)
+ return -EAGAIN;
+
+ if (seek->tuner != 0 ||
+ seek->type != V4L2_TUNER_RADIO)
+ return -EINVAL;
+
+ si476x_core_lock(radio->core);
+
+ if (!rangelow) {
+ err = regmap_read(radio->core->regmap,
+ SI476X_PROP_SEEK_BAND_BOTTOM,
+ &rangelow);
+ if (err)
+ goto unlock;
+ rangelow = si476x_to_v4l2(radio->core, rangelow);
+ }
+ if (!rangehigh) {
+ err = regmap_read(radio->core->regmap,
+ SI476X_PROP_SEEK_BAND_TOP,
+ &rangehigh);
+ if (err)
+ goto unlock;
+ rangehigh = si476x_to_v4l2(radio->core, rangehigh);
+ }
+
+ if (rangelow > rangehigh) {
+ err = -EINVAL;
+ goto unlock;
+ }
+
+ if (si476x_radio_range_is_inside_of_the_band(rangelow, rangehigh,
+ SI476X_BAND_FM)) {
+ func = SI476X_FUNC_FM_RECEIVER;
+
+ } else if (si476x_core_has_am(radio->core) &&
+ si476x_radio_range_is_inside_of_the_band(rangelow, rangehigh,
+ SI476X_BAND_AM)) {
+ func = SI476X_FUNC_AM_RECEIVER;
+ } else {
+ err = -EINVAL;
+ goto unlock;
+ }
+
+ err = si476x_radio_change_func(radio, func);
+ if (err < 0)
+ goto unlock;
+
+ if (seek->rangehigh) {
+ err = regmap_write(radio->core->regmap,
+ SI476X_PROP_SEEK_BAND_TOP,
+ v4l2_to_si476x(radio->core,
+ seek->rangehigh));
+ if (err)
+ goto unlock;
+ }
+ if (seek->rangelow) {
+ err = regmap_write(radio->core->regmap,
+ SI476X_PROP_SEEK_BAND_BOTTOM,
+ v4l2_to_si476x(radio->core,
+ seek->rangelow));
+ if (err)
+ goto unlock;
+ }
+ if (seek->spacing) {
+ err = regmap_write(radio->core->regmap,
+ SI476X_PROP_SEEK_FREQUENCY_SPACING,
+ v4l2_to_si476x(radio->core,
+ seek->spacing));
+ if (err)
+ goto unlock;
+ }
+
+ err = radio->ops->seek_start(radio->core,
+ seek->seek_upward,
+ seek->wrap_around);
+unlock:
+ si476x_core_unlock(radio->core);
+
+
+
+ return err;
+}
+
+static int si476x_radio_g_volatile_ctrl(struct v4l2_ctrl *ctrl)
+{
+ int retval;
+ struct si476x_radio *radio = v4l2_ctrl_handler_to_radio(ctrl->handler);
+
+ si476x_core_lock(radio->core);
+
+ switch (ctrl->id) {
+ case V4L2_CID_SI476X_INTERCHIP_LINK:
+ if (si476x_core_has_diversity(radio->core)) {
+ if (radio->ops->phase_diversity) {
+ retval = radio->ops->phase_div_status(radio->core);
+ if (retval < 0)
+ break;
+
+ ctrl->val = !!SI476X_PHDIV_STATUS_LINK_LOCKED(retval);
+ retval = 0;
+ break;
+ } else {
+ retval = -ENOTTY;
+ break;
+ }
+ }
+ retval = -EINVAL;
+ break;
+ default:
+ retval = -EINVAL;
+ break;
+ }
+ si476x_core_unlock(radio->core);
+ return retval;
+
+}
+
+static int si476x_radio_s_ctrl(struct v4l2_ctrl *ctrl)
+{
+ int retval;
+ enum si476x_phase_diversity_mode mode;
+ struct si476x_radio *radio = v4l2_ctrl_handler_to_radio(ctrl->handler);
+
+ si476x_core_lock(radio->core);
+
+ switch (ctrl->id) {
+ case V4L2_CID_SI476X_HARMONICS_COUNT:
+ retval = regmap_update_bits(radio->core->regmap,
+ SI476X_PROP_AUDIO_PWR_LINE_FILTER,
+ SI476X_PROP_PWR_HARMONICS_MASK,
+ ctrl->val);
+ break;
+ case V4L2_CID_POWER_LINE_FREQUENCY:
+ switch (ctrl->val) {
+ case V4L2_CID_POWER_LINE_FREQUENCY_DISABLED:
+ retval = regmap_update_bits(radio->core->regmap,
+ SI476X_PROP_AUDIO_PWR_LINE_FILTER,
+ SI476X_PROP_PWR_ENABLE_MASK,
+ 0);
+ break;
+ case V4L2_CID_POWER_LINE_FREQUENCY_50HZ:
+ retval = regmap_update_bits(radio->core->regmap,
+ SI476X_PROP_AUDIO_PWR_LINE_FILTER,
+ SI476X_PROP_PWR_GRID_MASK,
+ SI476X_PROP_PWR_GRID_50HZ);
+ break;
+ case V4L2_CID_POWER_LINE_FREQUENCY_60HZ:
+ retval = regmap_update_bits(radio->core->regmap,
+ SI476X_PROP_AUDIO_PWR_LINE_FILTER,
+ SI476X_PROP_PWR_GRID_MASK,
+ SI476X_PROP_PWR_GRID_60HZ);
+ break;
+ default:
+ retval = -EINVAL;
+ break;
+ }
+ break;
+ case V4L2_CID_SI476X_RSSI_THRESHOLD:
+ retval = regmap_write(radio->core->regmap,
+ SI476X_PROP_VALID_RSSI_THRESHOLD,
+ ctrl->val);
+ break;
+ case V4L2_CID_SI476X_SNR_THRESHOLD:
+ retval = regmap_write(radio->core->regmap,
+ SI476X_PROP_VALID_SNR_THRESHOLD,
+ ctrl->val);
+ break;
+ case V4L2_CID_SI476X_MAX_TUNE_ERROR:
+ retval = regmap_write(radio->core->regmap,
+ SI476X_PROP_VALID_MAX_TUNE_ERROR,
+ ctrl->val);
+ break;
+ case V4L2_CID_RDS_RECEPTION:
+ /*
+ * It looks like RDS related properties are
+ * inaccessible when tuner is in AM mode, so cache the
+ * changes
+ */
+ if (si476x_core_is_in_am_receiver_mode(radio->core))
+ regcache_cache_only(radio->core->regmap, true);
+
+ if (ctrl->val) {
+ retval = regmap_write(radio->core->regmap,
+ SI476X_PROP_FM_RDS_INTERRUPT_FIFO_COUNT,
+ radio->core->rds_fifo_depth);
+ if (retval < 0)
+ break;
+
+ if (radio->core->client->irq) {
+ retval = regmap_write(radio->core->regmap,
+ SI476X_PROP_FM_RDS_INTERRUPT_SOURCE,
+ SI476X_RDSRECV);
+ if (retval < 0)
+ break;
+ }
+
+ /* Drain RDS FIFO before enabling RDS processing */
+ retval = si476x_core_cmd_fm_rds_status(radio->core,
+ false,
+ true,
+ true,
+ NULL);
+ if (retval < 0)
+ break;
+
+ retval = regmap_update_bits(radio->core->regmap,
+ SI476X_PROP_FM_RDS_CONFIG,
+ SI476X_PROP_RDSEN_MASK,
+ SI476X_PROP_RDSEN);
+ } else {
+ retval = regmap_update_bits(radio->core->regmap,
+ SI476X_PROP_FM_RDS_CONFIG,
+ SI476X_PROP_RDSEN_MASK,
+ !SI476X_PROP_RDSEN);
+ }
+
+ if (si476x_core_is_in_am_receiver_mode(radio->core))
+ regcache_cache_only(radio->core->regmap, false);
+ break;
+ case V4L2_CID_TUNE_DEEMPHASIS:
+ retval = regmap_write(radio->core->regmap,
+ SI476X_PROP_AUDIO_DEEMPHASIS,
+ ctrl->val);
+ break;
+
+ case V4L2_CID_SI476X_DIVERSITY_MODE:
+ mode = si476x_phase_diversity_idx_to_mode(ctrl->val);
+
+ if (mode == radio->core->diversity_mode) {
+ retval = 0;
+ break;
+ }
+
+ if (si476x_core_is_in_am_receiver_mode(radio->core)) {
+ /*
+ * Diversity cannot be configured while tuner
+ * is in AM mode so save the changes and carry on.
+ */
+ radio->core->diversity_mode = mode;
+ retval = 0;
+ } else {
+ retval = radio->ops->phase_diversity(radio->core, mode);
+ if (!retval)
+ radio->core->diversity_mode = mode;
+ }
+ break;
+
+ default:
+ retval = -EINVAL;
+ break;
+ }
+
+ si476x_core_unlock(radio->core);
+
+ return retval;
+}
+
+#ifdef CONFIG_VIDEO_ADV_DEBUG
+static int si476x_radio_g_register(struct file *file, void *fh,
+ struct v4l2_dbg_register *reg)
+{
+ int err;
+ unsigned int value;
+ struct si476x_radio *radio = video_drvdata(file);
+
+ si476x_core_lock(radio->core);
+ reg->size = 2;
+ err = regmap_read(radio->core->regmap,
+ (unsigned int)reg->reg, &value);
+ reg->val = value;
+ si476x_core_unlock(radio->core);
+
+ return err;
+}
+static int si476x_radio_s_register(struct file *file, void *fh,
+ const struct v4l2_dbg_register *reg)
+{
+
+ int err;
+ struct si476x_radio *radio = video_drvdata(file);
+
+ si476x_core_lock(radio->core);
+ err = regmap_write(radio->core->regmap,
+ (unsigned int)reg->reg,
+ (unsigned int)reg->val);
+ si476x_core_unlock(radio->core);
+
+ return err;
+}
+#endif
+
+static int si476x_radio_fops_open(struct file *file)
+{
+ struct si476x_radio *radio = video_drvdata(file);
+ int err;
+
+ err = v4l2_fh_open(file);
+ if (err)
+ return err;
+
+ if (v4l2_fh_is_singular_file(file)) {
+ si476x_core_lock(radio->core);
+ err = si476x_core_set_power_state(radio->core,
+ SI476X_POWER_UP_FULL);
+ if (err < 0)
+ goto done;
+
+ err = si476x_radio_do_post_powerup_init(radio,
+ radio->core->power_up_parameters.func);
+ if (err < 0)
+ goto power_down;
+
+ err = si476x_radio_pretune(radio,
+ radio->core->power_up_parameters.func);
+ if (err < 0)
+ goto power_down;
+
+ si476x_core_unlock(radio->core);
+ /*Must be done after si476x_core_unlock to prevent a deadlock*/
+ v4l2_ctrl_handler_setup(&radio->ctrl_handler);
+ }
+
+ return err;
+
+power_down:
+ si476x_core_set_power_state(radio->core,
+ SI476X_POWER_DOWN);
+done:
+ si476x_core_unlock(radio->core);
+ v4l2_fh_release(file);
+
+ return err;
+}
+
+static int si476x_radio_fops_release(struct file *file)
+{
+ struct si476x_radio *radio = video_drvdata(file);
+
+ if (v4l2_fh_is_singular_file(file) &&
+ atomic_read(&radio->core->is_alive))
+ si476x_core_set_power_state(radio->core,
+ SI476X_POWER_DOWN);
+
+ return v4l2_fh_release(file);
+}
+
+static ssize_t si476x_radio_fops_read(struct file *file, char __user *buf,
+ size_t count, loff_t *ppos)
+{
+ ssize_t rval;
+ size_t fifo_len;
+ unsigned int copied;
+
+ struct si476x_radio *radio = video_drvdata(file);
+
+ /* block if no new data available */
+ if (kfifo_is_empty(&radio->core->rds_fifo)) {
+ if (file->f_flags & O_NONBLOCK)
+ return -EWOULDBLOCK;
+
+ rval = wait_event_interruptible(radio->core->rds_read_queue,
+ (!kfifo_is_empty(&radio->core->rds_fifo) ||
+ !atomic_read(&radio->core->is_alive)));
+ if (rval < 0)
+ return -EINTR;
+
+ if (!atomic_read(&radio->core->is_alive))
+ return -ENODEV;
+ }
+
+ fifo_len = kfifo_len(&radio->core->rds_fifo);
+
+ if (kfifo_to_user(&radio->core->rds_fifo, buf,
+ min(fifo_len, count),
+ &copied) != 0) {
+ dev_warn(&radio->videodev.dev,
+ "Error during FIFO to userspace copy\n");
+ rval = -EIO;
+ } else {
+ rval = (ssize_t)copied;
+ }
+
+ return rval;
+}
+
+static __poll_t si476x_radio_fops_poll(struct file *file,
+ struct poll_table_struct *pts)
+{
+ struct si476x_radio *radio = video_drvdata(file);
+ __poll_t req_events = poll_requested_events(pts);
+ __poll_t err = v4l2_ctrl_poll(file, pts);
+
+ if (req_events & (EPOLLIN | EPOLLRDNORM)) {
+ if (atomic_read(&radio->core->is_alive))
+ poll_wait(file, &radio->core->rds_read_queue, pts);
+
+ if (!atomic_read(&radio->core->is_alive))
+ err = EPOLLHUP;
+
+ if (!kfifo_is_empty(&radio->core->rds_fifo))
+ err = EPOLLIN | EPOLLRDNORM;
+ }
+
+ return err;
+}
+
+static const struct v4l2_file_operations si476x_fops = {
+ .owner = THIS_MODULE,
+ .read = si476x_radio_fops_read,
+ .poll = si476x_radio_fops_poll,
+ .unlocked_ioctl = video_ioctl2,
+ .open = si476x_radio_fops_open,
+ .release = si476x_radio_fops_release,
+};
+
+
+static const struct v4l2_ioctl_ops si4761_ioctl_ops = {
+ .vidioc_querycap = si476x_radio_querycap,
+ .vidioc_g_tuner = si476x_radio_g_tuner,
+ .vidioc_s_tuner = si476x_radio_s_tuner,
+
+ .vidioc_g_frequency = si476x_radio_g_frequency,
+ .vidioc_s_frequency = si476x_radio_s_frequency,
+ .vidioc_s_hw_freq_seek = si476x_radio_s_hw_freq_seek,
+ .vidioc_enum_freq_bands = si476x_radio_enum_freq_bands,
+
+ .vidioc_subscribe_event = v4l2_ctrl_subscribe_event,
+ .vidioc_unsubscribe_event = v4l2_event_unsubscribe,
+
+#ifdef CONFIG_VIDEO_ADV_DEBUG
+ .vidioc_g_register = si476x_radio_g_register,
+ .vidioc_s_register = si476x_radio_s_register,
+#endif
+};
+
+
+static const struct video_device si476x_viddev_template = {
+ .fops = &si476x_fops,
+ .name = DRIVER_NAME,
+ .release = video_device_release_empty,
+};
+
+
+
+static ssize_t si476x_radio_read_acf_blob(struct file *file,
+ char __user *user_buf,
+ size_t count, loff_t *ppos)
+{
+ int err;
+ struct si476x_radio *radio = file->private_data;
+ struct si476x_acf_status_report report;
+
+ si476x_core_lock(radio->core);
+ if (radio->ops->acf_status)
+ err = radio->ops->acf_status(radio->core, &report);
+ else
+ err = -ENOENT;
+ si476x_core_unlock(radio->core);
+
+ if (err < 0)
+ return err;
+
+ return simple_read_from_buffer(user_buf, count, ppos, &report,
+ sizeof(report));
+}
+
+static const struct file_operations radio_acf_fops = {
+ .open = simple_open,
+ .llseek = default_llseek,
+ .read = si476x_radio_read_acf_blob,
+};
+
+static ssize_t si476x_radio_read_rds_blckcnt_blob(struct file *file,
+ char __user *user_buf,
+ size_t count, loff_t *ppos)
+{
+ int err;
+ struct si476x_radio *radio = file->private_data;
+ struct si476x_rds_blockcount_report report;
+
+ si476x_core_lock(radio->core);
+ if (radio->ops->rds_blckcnt)
+ err = radio->ops->rds_blckcnt(radio->core, true,
+ &report);
+ else
+ err = -ENOENT;
+ si476x_core_unlock(radio->core);
+
+ if (err < 0)
+ return err;
+
+ return simple_read_from_buffer(user_buf, count, ppos, &report,
+ sizeof(report));
+}
+
+static const struct file_operations radio_rds_blckcnt_fops = {
+ .open = simple_open,
+ .llseek = default_llseek,
+ .read = si476x_radio_read_rds_blckcnt_blob,
+};
+
+static ssize_t si476x_radio_read_agc_blob(struct file *file,
+ char __user *user_buf,
+ size_t count, loff_t *ppos)
+{
+ int err;
+ struct si476x_radio *radio = file->private_data;
+ struct si476x_agc_status_report report;
+
+ si476x_core_lock(radio->core);
+ if (radio->ops->rds_blckcnt)
+ err = radio->ops->agc_status(radio->core, &report);
+ else
+ err = -ENOENT;
+ si476x_core_unlock(radio->core);
+
+ if (err < 0)
+ return err;
+
+ return simple_read_from_buffer(user_buf, count, ppos, &report,
+ sizeof(report));
+}
+
+static const struct file_operations radio_agc_fops = {
+ .open = simple_open,
+ .llseek = default_llseek,
+ .read = si476x_radio_read_agc_blob,
+};
+
+static ssize_t si476x_radio_read_rsq_blob(struct file *file,
+ char __user *user_buf,
+ size_t count, loff_t *ppos)
+{
+ int err;
+ struct si476x_radio *radio = file->private_data;
+ struct si476x_rsq_status_report report;
+ struct si476x_rsq_status_args args = {
+ .primary = false,
+ .rsqack = false,
+ .attune = false,
+ .cancel = false,
+ .stcack = false,
+ };
+
+ si476x_core_lock(radio->core);
+ if (radio->ops->rds_blckcnt)
+ err = radio->ops->rsq_status(radio->core, &args, &report);
+ else
+ err = -ENOENT;
+ si476x_core_unlock(radio->core);
+
+ if (err < 0)
+ return err;
+
+ return simple_read_from_buffer(user_buf, count, ppos, &report,
+ sizeof(report));
+}
+
+static const struct file_operations radio_rsq_fops = {
+ .open = simple_open,
+ .llseek = default_llseek,
+ .read = si476x_radio_read_rsq_blob,
+};
+
+static ssize_t si476x_radio_read_rsq_primary_blob(struct file *file,
+ char __user *user_buf,
+ size_t count, loff_t *ppos)
+{
+ int err;
+ struct si476x_radio *radio = file->private_data;
+ struct si476x_rsq_status_report report;
+ struct si476x_rsq_status_args args = {
+ .primary = true,
+ .rsqack = false,
+ .attune = false,
+ .cancel = false,
+ .stcack = false,
+ };
+
+ si476x_core_lock(radio->core);
+ if (radio->ops->rds_blckcnt)
+ err = radio->ops->rsq_status(radio->core, &args, &report);
+ else
+ err = -ENOENT;
+ si476x_core_unlock(radio->core);
+
+ if (err < 0)
+ return err;
+
+ return simple_read_from_buffer(user_buf, count, ppos, &report,
+ sizeof(report));
+}
+
+static const struct file_operations radio_rsq_primary_fops = {
+ .open = simple_open,
+ .llseek = default_llseek,
+ .read = si476x_radio_read_rsq_primary_blob,
+};
+
+
+static void si476x_radio_init_debugfs(struct si476x_radio *radio)
+{
+ radio->debugfs = debugfs_create_dir(dev_name(radio->v4l2dev.dev), NULL);
+
+ debugfs_create_file("acf", S_IRUGO, radio->debugfs, radio,
+ &radio_acf_fops);
+
+ debugfs_create_file("rds_blckcnt", S_IRUGO, radio->debugfs, radio,
+ &radio_rds_blckcnt_fops);
+
+ debugfs_create_file("agc", S_IRUGO, radio->debugfs, radio,
+ &radio_agc_fops);
+
+ debugfs_create_file("rsq", S_IRUGO, radio->debugfs, radio,
+ &radio_rsq_fops);
+
+ debugfs_create_file("rsq_primary", S_IRUGO, radio->debugfs, radio,
+ &radio_rsq_primary_fops);
+}
+
+
+static int si476x_radio_add_new_custom(struct si476x_radio *radio,
+ enum si476x_ctrl_idx idx)
+{
+ int rval;
+ struct v4l2_ctrl *ctrl;
+
+ ctrl = v4l2_ctrl_new_custom(&radio->ctrl_handler,
+ &si476x_ctrls[idx],
+ NULL);
+ rval = radio->ctrl_handler.error;
+ if (ctrl == NULL && rval)
+ dev_err(radio->v4l2dev.dev,
+ "Could not initialize '%s' control %d\n",
+ si476x_ctrls[idx].name, rval);
+
+ return rval;
+}
+
+static int si476x_radio_probe(struct platform_device *pdev)
+{
+ int rval;
+ struct si476x_radio *radio;
+ struct v4l2_ctrl *ctrl;
+
+ static atomic_t instance = ATOMIC_INIT(0);
+
+ radio = devm_kzalloc(&pdev->dev, sizeof(*radio), GFP_KERNEL);
+ if (!radio)
+ return -ENOMEM;
+
+ radio->core = i2c_mfd_cell_to_core(&pdev->dev);
+
+ v4l2_device_set_name(&radio->v4l2dev, DRIVER_NAME, &instance);
+
+ rval = v4l2_device_register(&pdev->dev, &radio->v4l2dev);
+ if (rval) {
+ dev_err(&pdev->dev, "Cannot register v4l2_device.\n");
+ return rval;
+ }
+
+ memcpy(&radio->videodev, &si476x_viddev_template,
+ sizeof(struct video_device));
+
+ radio->videodev.v4l2_dev = &radio->v4l2dev;
+ radio->videodev.ioctl_ops = &si4761_ioctl_ops;
+ radio->videodev.device_caps = V4L2_CAP_TUNER | V4L2_CAP_RADIO |
+ V4L2_CAP_HW_FREQ_SEEK;
+
+ si476x_core_lock(radio->core);
+ if (!si476x_core_is_a_secondary_tuner(radio->core))
+ radio->videodev.device_caps |= V4L2_CAP_RDS_CAPTURE |
+ V4L2_CAP_READWRITE;
+ si476x_core_unlock(radio->core);
+
+ video_set_drvdata(&radio->videodev, radio);
+ platform_set_drvdata(pdev, radio);
+
+
+ radio->v4l2dev.ctrl_handler = &radio->ctrl_handler;
+ v4l2_ctrl_handler_init(&radio->ctrl_handler,
+ 1 + ARRAY_SIZE(si476x_ctrls));
+
+ if (si476x_core_has_am(radio->core)) {
+ ctrl = v4l2_ctrl_new_std_menu(&radio->ctrl_handler,
+ &si476x_ctrl_ops,
+ V4L2_CID_POWER_LINE_FREQUENCY,
+ V4L2_CID_POWER_LINE_FREQUENCY_60HZ,
+ 0, 0);
+ rval = radio->ctrl_handler.error;
+ if (ctrl == NULL && rval) {
+ dev_err(&pdev->dev, "Could not initialize V4L2_CID_POWER_LINE_FREQUENCY control %d\n",
+ rval);
+ goto exit;
+ }
+
+ rval = si476x_radio_add_new_custom(radio,
+ SI476X_IDX_HARMONICS_COUNT);
+ if (rval < 0)
+ goto exit;
+ }
+
+ rval = si476x_radio_add_new_custom(radio, SI476X_IDX_RSSI_THRESHOLD);
+ if (rval < 0)
+ goto exit;
+
+ rval = si476x_radio_add_new_custom(radio, SI476X_IDX_SNR_THRESHOLD);
+ if (rval < 0)
+ goto exit;
+
+ rval = si476x_radio_add_new_custom(radio, SI476X_IDX_MAX_TUNE_ERROR);
+ if (rval < 0)
+ goto exit;
+
+ ctrl = v4l2_ctrl_new_std_menu(&radio->ctrl_handler,
+ &si476x_ctrl_ops,
+ V4L2_CID_TUNE_DEEMPHASIS,
+ V4L2_DEEMPHASIS_75_uS, 0, 0);
+ rval = radio->ctrl_handler.error;
+ if (ctrl == NULL && rval) {
+ dev_err(&pdev->dev, "Could not initialize V4L2_CID_TUNE_DEEMPHASIS control %d\n",
+ rval);
+ goto exit;
+ }
+
+ ctrl = v4l2_ctrl_new_std(&radio->ctrl_handler, &si476x_ctrl_ops,
+ V4L2_CID_RDS_RECEPTION,
+ 0, 1, 1, 1);
+ rval = radio->ctrl_handler.error;
+ if (ctrl == NULL && rval) {
+ dev_err(&pdev->dev, "Could not initialize V4L2_CID_RDS_RECEPTION control %d\n",
+ rval);
+ goto exit;
+ }
+
+ if (si476x_core_has_diversity(radio->core)) {
+ si476x_ctrls[SI476X_IDX_DIVERSITY_MODE].def =
+ si476x_phase_diversity_mode_to_idx(radio->core->diversity_mode);
+ rval = si476x_radio_add_new_custom(radio, SI476X_IDX_DIVERSITY_MODE);
+ if (rval < 0)
+ goto exit;
+
+ rval = si476x_radio_add_new_custom(radio, SI476X_IDX_INTERCHIP_LINK);
+ if (rval < 0)
+ goto exit;
+ }
+
+ /* register video device */
+ rval = video_register_device(&radio->videodev, VFL_TYPE_RADIO, -1);
+ if (rval < 0) {
+ dev_err(&pdev->dev, "Could not register video device\n");
+ goto exit;
+ }
+
+ si476x_radio_init_debugfs(radio);
+
+ return 0;
+exit:
+ v4l2_ctrl_handler_free(radio->videodev.ctrl_handler);
+ v4l2_device_unregister(&radio->v4l2dev);
+ return rval;
+}
+
+static void si476x_radio_remove(struct platform_device *pdev)
+{
+ struct si476x_radio *radio = platform_get_drvdata(pdev);
+
+ v4l2_ctrl_handler_free(radio->videodev.ctrl_handler);
+ video_unregister_device(&radio->videodev);
+ v4l2_device_unregister(&radio->v4l2dev);
+ debugfs_remove_recursive(radio->debugfs);
+}
+
+MODULE_ALIAS("platform:si476x-radio");
+
+static struct platform_driver si476x_radio_driver = {
+ .driver = {
+ .name = DRIVER_NAME,
+ },
+ .probe = si476x_radio_probe,
+ .remove = si476x_radio_remove,
+};
+module_platform_driver(si476x_radio_driver);
+
+MODULE_AUTHOR("Andrey Smirnov <andrew.smirnov@gmail.com>");
+MODULE_DESCRIPTION("Driver for Si4761/64/68 AM/FM Radio MFD Cell");
+MODULE_LICENSE("GPL");
diff --git a/drivers/media/radio/radio-tea5764.c b/drivers/media/radio/radio-tea5764.c
new file mode 100644
index 000000000..d9547f625
--- /dev/null
+++ b/drivers/media/radio/radio-tea5764.c
@@ -0,0 +1,529 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * driver/media/radio/radio-tea5764.c
+ *
+ * Driver for TEA5764 radio chip for linux 2.6.
+ * This driver is for TEA5764 chip from NXP, used in EZX phones from Motorola.
+ * The I2C protocol is used for communicate with chip.
+ *
+ * Based in radio-tea5761.c Copyright (C) 2005 Nokia Corporation
+ *
+ * Copyright (c) 2008 Fabio Belavenuto <belavenuto@gmail.com>
+ *
+ * History:
+ * 2008-12-06 Fabio Belavenuto <belavenuto@gmail.com>
+ * initial code
+ *
+ * TODO:
+ * add platform_data support for IRQs platform dependencies
+ * add RDS support
+ */
+#include <linux/kernel.h>
+#include <linux/slab.h>
+#include <linux/module.h>
+#include <linux/init.h> /* Initdata */
+#include <linux/videodev2.h> /* kernel radio structs */
+#include <linux/i2c.h> /* I2C */
+#include <media/v4l2-common.h>
+#include <media/v4l2-ioctl.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-event.h>
+
+#define DRIVER_VERSION "0.0.2"
+
+#define DRIVER_AUTHOR "Fabio Belavenuto <belavenuto@gmail.com>"
+#define DRIVER_DESC "A driver for the TEA5764 radio chip for EZX Phones."
+
+#define PINFO(format, ...)\
+ printk(KERN_INFO KBUILD_MODNAME ": "\
+ DRIVER_VERSION ": " format "\n", ## __VA_ARGS__)
+#define PWARN(format, ...)\
+ printk(KERN_WARNING KBUILD_MODNAME ": "\
+ DRIVER_VERSION ": " format "\n", ## __VA_ARGS__)
+# define PDEBUG(format, ...)\
+ printk(KERN_DEBUG KBUILD_MODNAME ": "\
+ DRIVER_VERSION ": " format "\n", ## __VA_ARGS__)
+
+/* Frequency limits in MHz -- these are European values. For Japanese
+devices, that would be 76000 and 91000. */
+#define FREQ_MIN 87500U
+#define FREQ_MAX 108000U
+#define FREQ_MUL 16
+
+/* TEA5764 registers */
+#define TEA5764_MANID 0x002b
+#define TEA5764_CHIPID 0x5764
+
+#define TEA5764_INTREG_BLMSK 0x0001
+#define TEA5764_INTREG_FRRMSK 0x0002
+#define TEA5764_INTREG_LEVMSK 0x0008
+#define TEA5764_INTREG_IFMSK 0x0010
+#define TEA5764_INTREG_BLMFLAG 0x0100
+#define TEA5764_INTREG_FRRFLAG 0x0200
+#define TEA5764_INTREG_LEVFLAG 0x0800
+#define TEA5764_INTREG_IFFLAG 0x1000
+
+#define TEA5764_FRQSET_SUD 0x8000
+#define TEA5764_FRQSET_SM 0x4000
+
+#define TEA5764_TNCTRL_PUPD1 0x8000
+#define TEA5764_TNCTRL_PUPD0 0x4000
+#define TEA5764_TNCTRL_BLIM 0x2000
+#define TEA5764_TNCTRL_SWPM 0x1000
+#define TEA5764_TNCTRL_IFCTC 0x0800
+#define TEA5764_TNCTRL_AFM 0x0400
+#define TEA5764_TNCTRL_SMUTE 0x0200
+#define TEA5764_TNCTRL_SNC 0x0100
+#define TEA5764_TNCTRL_MU 0x0080
+#define TEA5764_TNCTRL_SSL1 0x0040
+#define TEA5764_TNCTRL_SSL0 0x0020
+#define TEA5764_TNCTRL_HLSI 0x0010
+#define TEA5764_TNCTRL_MST 0x0008
+#define TEA5764_TNCTRL_SWP 0x0004
+#define TEA5764_TNCTRL_DTC 0x0002
+#define TEA5764_TNCTRL_AHLSI 0x0001
+
+#define TEA5764_TUNCHK_LEVEL(x) (((x) & 0x00F0) >> 4)
+#define TEA5764_TUNCHK_IFCNT(x) (((x) & 0xFE00) >> 9)
+#define TEA5764_TUNCHK_TUNTO 0x0100
+#define TEA5764_TUNCHK_LD 0x0008
+#define TEA5764_TUNCHK_STEREO 0x0004
+
+#define TEA5764_TESTREG_TRIGFR 0x0800
+
+struct tea5764_regs {
+ u16 intreg; /* INTFLAG & INTMSK */
+ u16 frqset; /* FRQSETMSB & FRQSETLSB */
+ u16 tnctrl; /* TNCTRL1 & TNCTRL2 */
+ u16 frqchk; /* FRQCHKMSB & FRQCHKLSB */
+ u16 tunchk; /* IFCHK & LEVCHK */
+ u16 testreg; /* TESTBITS & TESTMODE */
+ u16 rdsstat; /* RDSSTAT1 & RDSSTAT2 */
+ u16 rdslb; /* RDSLBMSB & RDSLBLSB */
+ u16 rdspb; /* RDSPBMSB & RDSPBLSB */
+ u16 rdsbc; /* RDSBBC & RDSGBC */
+ u16 rdsctrl; /* RDSCTRL1 & RDSCTRL2 */
+ u16 rdsbbl; /* PAUSEDET & RDSBBL */
+ u16 manid; /* MANID1 & MANID2 */
+ u16 chipid; /* CHIPID1 & CHIPID2 */
+} __attribute__ ((packed));
+
+struct tea5764_write_regs {
+ u8 intreg; /* INTMSK */
+ __be16 frqset; /* FRQSETMSB & FRQSETLSB */
+ __be16 tnctrl; /* TNCTRL1 & TNCTRL2 */
+ __be16 testreg; /* TESTBITS & TESTMODE */
+ __be16 rdsctrl; /* RDSCTRL1 & RDSCTRL2 */
+ __be16 rdsbbl; /* PAUSEDET & RDSBBL */
+} __attribute__ ((packed));
+
+#ifdef CONFIG_RADIO_TEA5764_XTAL
+#define RADIO_TEA5764_XTAL 1
+#else
+#define RADIO_TEA5764_XTAL 0
+#endif
+
+static int radio_nr = -1;
+static int use_xtal = RADIO_TEA5764_XTAL;
+
+struct tea5764_device {
+ struct v4l2_device v4l2_dev;
+ struct v4l2_ctrl_handler ctrl_handler;
+ struct i2c_client *i2c_client;
+ struct video_device vdev;
+ struct tea5764_regs regs;
+ struct mutex mutex;
+};
+
+/* I2C code related */
+static int tea5764_i2c_read(struct tea5764_device *radio)
+{
+ int i;
+ u16 *p = (u16 *) &radio->regs;
+
+ struct i2c_msg msgs[1] = {
+ { .addr = radio->i2c_client->addr,
+ .flags = I2C_M_RD,
+ .len = sizeof(radio->regs),
+ .buf = (void *)&radio->regs
+ },
+ };
+ if (i2c_transfer(radio->i2c_client->adapter, msgs, 1) != 1)
+ return -EIO;
+ for (i = 0; i < sizeof(struct tea5764_regs) / sizeof(u16); i++)
+ p[i] = __be16_to_cpu((__force __be16)p[i]);
+
+ return 0;
+}
+
+static int tea5764_i2c_write(struct tea5764_device *radio)
+{
+ struct tea5764_write_regs wr;
+ struct tea5764_regs *r = &radio->regs;
+ struct i2c_msg msgs[1] = {
+ {
+ .addr = radio->i2c_client->addr,
+ .len = sizeof(wr),
+ .buf = (void *)&wr
+ },
+ };
+ 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);
+ wr.rdsctrl = __cpu_to_be16(r->rdsctrl);
+ wr.rdsbbl = __cpu_to_be16(r->rdsbbl);
+ if (i2c_transfer(radio->i2c_client->adapter, msgs, 1) != 1)
+ return -EIO;
+ return 0;
+}
+
+static void tea5764_power_up(struct tea5764_device *radio)
+{
+ struct tea5764_regs *r = &radio->regs;
+
+ if (!(r->tnctrl & TEA5764_TNCTRL_PUPD0)) {
+ r->tnctrl &= ~(TEA5764_TNCTRL_AFM | TEA5764_TNCTRL_MU |
+ TEA5764_TNCTRL_HLSI);
+ if (!use_xtal)
+ r->testreg |= TEA5764_TESTREG_TRIGFR;
+ else
+ r->testreg &= ~TEA5764_TESTREG_TRIGFR;
+
+ r->tnctrl |= TEA5764_TNCTRL_PUPD0;
+ tea5764_i2c_write(radio);
+ }
+}
+
+static void tea5764_power_down(struct tea5764_device *radio)
+{
+ struct tea5764_regs *r = &radio->regs;
+
+ if (r->tnctrl & TEA5764_TNCTRL_PUPD0) {
+ r->tnctrl &= ~TEA5764_TNCTRL_PUPD0;
+ tea5764_i2c_write(radio);
+ }
+}
+
+static void tea5764_set_freq(struct tea5764_device *radio, int freq)
+{
+ struct tea5764_regs *r = &radio->regs;
+
+ /* formula: (freq [+ or -] 225000) / 8192 */
+ if (r->tnctrl & TEA5764_TNCTRL_HLSI)
+ r->frqset = (freq + 225000) / 8192;
+ else
+ r->frqset = (freq - 225000) / 8192;
+}
+
+static int tea5764_get_freq(struct tea5764_device *radio)
+{
+ struct tea5764_regs *r = &radio->regs;
+
+ if (r->tnctrl & TEA5764_TNCTRL_HLSI)
+ return (r->frqchk * 8192) - 225000;
+ else
+ return (r->frqchk * 8192) + 225000;
+}
+
+/* tune an frequency, freq is defined by v4l's TUNER_LOW, i.e. 1/16th kHz */
+static void tea5764_tune(struct tea5764_device *radio, int freq)
+{
+ tea5764_set_freq(radio, freq);
+ if (tea5764_i2c_write(radio))
+ PWARN("Could not set frequency!");
+}
+
+static void tea5764_set_audout_mode(struct tea5764_device *radio, int audmode)
+{
+ struct tea5764_regs *r = &radio->regs;
+ int tnctrl = r->tnctrl;
+
+ if (audmode == V4L2_TUNER_MODE_MONO)
+ r->tnctrl |= TEA5764_TNCTRL_MST;
+ else
+ r->tnctrl &= ~TEA5764_TNCTRL_MST;
+ if (tnctrl != r->tnctrl)
+ tea5764_i2c_write(radio);
+}
+
+static int tea5764_get_audout_mode(struct tea5764_device *radio)
+{
+ struct tea5764_regs *r = &radio->regs;
+
+ if (r->tnctrl & TEA5764_TNCTRL_MST)
+ return V4L2_TUNER_MODE_MONO;
+ else
+ return V4L2_TUNER_MODE_STEREO;
+}
+
+static void tea5764_mute(struct tea5764_device *radio, int on)
+{
+ struct tea5764_regs *r = &radio->regs;
+ int tnctrl = r->tnctrl;
+
+ if (on)
+ r->tnctrl |= TEA5764_TNCTRL_MU;
+ else
+ r->tnctrl &= ~TEA5764_TNCTRL_MU;
+ if (tnctrl != r->tnctrl)
+ tea5764_i2c_write(radio);
+}
+
+/* V4L2 vidioc */
+static int vidioc_querycap(struct file *file, void *priv,
+ struct v4l2_capability *v)
+{
+ struct tea5764_device *radio = video_drvdata(file);
+ struct video_device *dev = &radio->vdev;
+
+ strscpy(v->driver, dev->dev.driver->name, sizeof(v->driver));
+ strscpy(v->card, dev->name, sizeof(v->card));
+ snprintf(v->bus_info, sizeof(v->bus_info),
+ "I2C:%s", dev_name(&dev->dev));
+ return 0;
+}
+
+static int vidioc_g_tuner(struct file *file, void *priv,
+ struct v4l2_tuner *v)
+{
+ struct tea5764_device *radio = video_drvdata(file);
+ struct tea5764_regs *r = &radio->regs;
+
+ if (v->index > 0)
+ return -EINVAL;
+
+ strscpy(v->name, "FM", sizeof(v->name));
+ v->type = V4L2_TUNER_RADIO;
+ tea5764_i2c_read(radio);
+ v->rangelow = FREQ_MIN * FREQ_MUL;
+ v->rangehigh = FREQ_MAX * FREQ_MUL;
+ v->capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO;
+ if (r->tunchk & TEA5764_TUNCHK_STEREO)
+ v->rxsubchans = V4L2_TUNER_SUB_STEREO;
+ else
+ v->rxsubchans = V4L2_TUNER_SUB_MONO;
+ v->audmode = tea5764_get_audout_mode(radio);
+ v->signal = TEA5764_TUNCHK_LEVEL(r->tunchk) * 0xffff / 0xf;
+ v->afc = TEA5764_TUNCHK_IFCNT(r->tunchk);
+
+ return 0;
+}
+
+static int vidioc_s_tuner(struct file *file, void *priv,
+ const struct v4l2_tuner *v)
+{
+ struct tea5764_device *radio = video_drvdata(file);
+
+ if (v->index > 0)
+ return -EINVAL;
+
+ tea5764_set_audout_mode(radio, v->audmode);
+ return 0;
+}
+
+static int vidioc_s_frequency(struct file *file, void *priv,
+ const struct v4l2_frequency *f)
+{
+ struct tea5764_device *radio = video_drvdata(file);
+ unsigned freq = f->frequency;
+
+ if (f->tuner != 0 || f->type != V4L2_TUNER_RADIO)
+ return -EINVAL;
+ if (freq == 0) {
+ /* We special case this as a power down control. */
+ tea5764_power_down(radio);
+ /* Yes, that's what is returned in this case. This
+ whole special case is non-compliant and should really
+ be replaced with something better, but changing this
+ might well break code that depends on this behavior.
+ So we keep it as-is. */
+ return -EINVAL;
+ }
+ freq = clamp(freq, FREQ_MIN * FREQ_MUL, FREQ_MAX * FREQ_MUL);
+ tea5764_power_up(radio);
+ tea5764_tune(radio, (freq * 125) / 2);
+ return 0;
+}
+
+static int vidioc_g_frequency(struct file *file, void *priv,
+ struct v4l2_frequency *f)
+{
+ struct tea5764_device *radio = video_drvdata(file);
+ struct tea5764_regs *r = &radio->regs;
+
+ if (f->tuner != 0)
+ return -EINVAL;
+ tea5764_i2c_read(radio);
+ f->type = V4L2_TUNER_RADIO;
+ if (r->tnctrl & TEA5764_TNCTRL_PUPD0)
+ f->frequency = (tea5764_get_freq(radio) * 2) / 125;
+ else
+ f->frequency = 0;
+
+ return 0;
+}
+
+static int tea5764_s_ctrl(struct v4l2_ctrl *ctrl)
+{
+ struct tea5764_device *radio =
+ container_of(ctrl->handler, struct tea5764_device, ctrl_handler);
+
+ switch (ctrl->id) {
+ case V4L2_CID_AUDIO_MUTE:
+ tea5764_mute(radio, ctrl->val);
+ return 0;
+ }
+ return -EINVAL;
+}
+
+static const struct v4l2_ctrl_ops tea5764_ctrl_ops = {
+ .s_ctrl = tea5764_s_ctrl,
+};
+
+/* File system interface */
+static const struct v4l2_file_operations tea5764_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 tea5764_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,
+};
+
+/* V4L2 interface */
+static const struct video_device tea5764_radio_template = {
+ .name = "TEA5764 FM-Radio",
+ .fops = &tea5764_fops,
+ .ioctl_ops = &tea5764_ioctl_ops,
+ .release = video_device_release_empty,
+};
+
+/* I2C probe: check if the device exists and register with v4l if it is */
+static int tea5764_i2c_probe(struct i2c_client *client)
+{
+ struct tea5764_device *radio;
+ struct v4l2_device *v4l2_dev;
+ struct v4l2_ctrl_handler *hdl;
+ struct tea5764_regs *r;
+ int ret;
+
+ PDEBUG("probe");
+ radio = kzalloc_obj(struct tea5764_device);
+ if (!radio)
+ return -ENOMEM;
+
+ v4l2_dev = &radio->v4l2_dev;
+ ret = v4l2_device_register(&client->dev, v4l2_dev);
+ if (ret < 0) {
+ v4l2_err(v4l2_dev, "could not register v4l2_device\n");
+ goto errfr;
+ }
+
+ hdl = &radio->ctrl_handler;
+ v4l2_ctrl_handler_init(hdl, 1);
+ v4l2_ctrl_new_std(hdl, &tea5764_ctrl_ops,
+ V4L2_CID_AUDIO_MUTE, 0, 1, 1, 1);
+ v4l2_dev->ctrl_handler = hdl;
+ if (hdl->error) {
+ ret = hdl->error;
+ v4l2_err(v4l2_dev, "Could not register controls\n");
+ goto errunreg;
+ }
+
+ mutex_init(&radio->mutex);
+ radio->i2c_client = client;
+ ret = tea5764_i2c_read(radio);
+ if (ret)
+ goto errunreg;
+ r = &radio->regs;
+ PDEBUG("chipid = %04X, manid = %04X", r->chipid, r->manid);
+ if (r->chipid != TEA5764_CHIPID ||
+ (r->manid & 0x0fff) != TEA5764_MANID) {
+ PWARN("This chip is not a TEA5764!");
+ ret = -EINVAL;
+ goto errunreg;
+ }
+
+ radio->vdev = tea5764_radio_template;
+
+ i2c_set_clientdata(client, radio);
+ video_set_drvdata(&radio->vdev, radio);
+ radio->vdev.lock = &radio->mutex;
+ radio->vdev.v4l2_dev = v4l2_dev;
+ radio->vdev.device_caps = V4L2_CAP_TUNER | V4L2_CAP_RADIO;
+
+ /* initialize and power off the chip */
+ tea5764_i2c_read(radio);
+ tea5764_set_audout_mode(radio, V4L2_TUNER_MODE_STEREO);
+ tea5764_mute(radio, 1);
+ tea5764_power_down(radio);
+
+ ret = video_register_device(&radio->vdev, VFL_TYPE_RADIO, radio_nr);
+ if (ret < 0) {
+ PWARN("Could not register video device!");
+ goto errunreg;
+ }
+
+ PINFO("registered.");
+ return 0;
+errunreg:
+ v4l2_ctrl_handler_free(hdl);
+ v4l2_device_unregister(v4l2_dev);
+errfr:
+ kfree(radio);
+ return ret;
+}
+
+static void tea5764_i2c_remove(struct i2c_client *client)
+{
+ struct tea5764_device *radio = i2c_get_clientdata(client);
+
+ PDEBUG("remove");
+ if (radio) {
+ tea5764_power_down(radio);
+ video_unregister_device(&radio->vdev);
+ v4l2_ctrl_handler_free(&radio->ctrl_handler);
+ v4l2_device_unregister(&radio->v4l2_dev);
+ kfree(radio);
+ }
+}
+
+/* I2C subsystem interface */
+static const struct i2c_device_id tea5764_id[] = {
+ { .name = "radio-tea5764" },
+ { } /* Terminating entry */
+};
+MODULE_DEVICE_TABLE(i2c, tea5764_id);
+
+static struct i2c_driver tea5764_i2c_driver = {
+ .driver = {
+ .name = "radio-tea5764",
+ },
+ .probe = tea5764_i2c_probe,
+ .remove = tea5764_i2c_remove,
+ .id_table = tea5764_id,
+};
+
+module_i2c_driver(tea5764_i2c_driver);
+
+MODULE_AUTHOR(DRIVER_AUTHOR);
+MODULE_DESCRIPTION(DRIVER_DESC);
+MODULE_LICENSE("GPL");
+MODULE_VERSION(DRIVER_VERSION);
+
+module_param(use_xtal, int, 0);
+MODULE_PARM_DESC(use_xtal, "Chip have a xtal connected in board");
+module_param(radio_nr, int, 0);
+MODULE_PARM_DESC(radio_nr, "video4linux device number to use");
diff --git a/drivers/media/radio/radio-tea5777.c b/drivers/media/radio/radio-tea5777.c
new file mode 100644
index 000000000..fb9de7bbc
--- /dev/null
+++ b/drivers/media/radio/radio-tea5777.c
@@ -0,0 +1,587 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * v4l2 driver for TEA5777 Philips AM/FM radio tuner chips
+ *
+ * Copyright (c) 2012 Hans de Goede <hdegoede@redhat.com>
+ *
+ * Based on the ALSA driver for TEA5757/5759 Philips AM/FM radio tuner chips:
+ *
+ * Copyright (c) 2004 Jaroslav Kysela <perex@perex.cz>
+ */
+
+#include <linux/delay.h>
+#include <linux/init.h>
+#include <linux/module.h>
+#include <linux/sched.h>
+#include <linux/slab.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-dev.h>
+#include <media/v4l2-fh.h>
+#include <media/v4l2-ioctl.h>
+#include <media/v4l2-event.h>
+#include "radio-tea5777.h"
+
+MODULE_AUTHOR("Hans de Goede <perex@perex.cz>");
+MODULE_DESCRIPTION("Routines for control of TEA5777 Philips AM/FM radio tuner chips");
+MODULE_LICENSE("GPL");
+
+#define TEA5777_FM_IF 150 /* kHz */
+#define TEA5777_FM_FREQ_STEP 50 /* kHz */
+
+#define TEA5777_AM_IF 21 /* kHz */
+#define TEA5777_AM_FREQ_STEP 1 /* kHz */
+
+/* Write reg, common bits */
+#define TEA5777_W_MUTE_MASK (1LL << 47)
+#define TEA5777_W_MUTE_SHIFT 47
+#define TEA5777_W_AM_FM_MASK (1LL << 46)
+#define TEA5777_W_AM_FM_SHIFT 46
+#define TEA5777_W_STB_MASK (1LL << 45)
+#define TEA5777_W_STB_SHIFT 45
+
+#define TEA5777_W_IFCE_MASK (1LL << 29)
+#define TEA5777_W_IFCE_SHIFT 29
+#define TEA5777_W_IFW_MASK (1LL << 28)
+#define TEA5777_W_IFW_SHIFT 28
+#define TEA5777_W_HILO_MASK (1LL << 27)
+#define TEA5777_W_HILO_SHIFT 27
+#define TEA5777_W_DBUS_MASK (1LL << 26)
+#define TEA5777_W_DBUS_SHIFT 26
+
+#define TEA5777_W_INTEXT_MASK (1LL << 24)
+#define TEA5777_W_INTEXT_SHIFT 24
+#define TEA5777_W_P1_MASK (1LL << 23)
+#define TEA5777_W_P1_SHIFT 23
+#define TEA5777_W_P0_MASK (1LL << 22)
+#define TEA5777_W_P0_SHIFT 22
+#define TEA5777_W_PEN1_MASK (1LL << 21)
+#define TEA5777_W_PEN1_SHIFT 21
+#define TEA5777_W_PEN0_MASK (1LL << 20)
+#define TEA5777_W_PEN0_SHIFT 20
+
+#define TEA5777_W_CHP0_MASK (1LL << 18)
+#define TEA5777_W_CHP0_SHIFT 18
+#define TEA5777_W_DEEM_MASK (1LL << 17)
+#define TEA5777_W_DEEM_SHIFT 17
+
+#define TEA5777_W_SEARCH_MASK (1LL << 7)
+#define TEA5777_W_SEARCH_SHIFT 7
+#define TEA5777_W_PROGBLIM_MASK (1LL << 6)
+#define TEA5777_W_PROGBLIM_SHIFT 6
+#define TEA5777_W_UPDWN_MASK (1LL << 5)
+#define TEA5777_W_UPDWN_SHIFT 5
+#define TEA5777_W_SLEV_MASK (3LL << 3)
+#define TEA5777_W_SLEV_SHIFT 3
+
+/* Write reg, FM specific bits */
+#define TEA5777_W_FM_PLL_MASK (0x1fffLL << 32)
+#define TEA5777_W_FM_PLL_SHIFT 32
+#define TEA5777_W_FM_FREF_MASK (0x03LL << 30)
+#define TEA5777_W_FM_FREF_SHIFT 30
+#define TEA5777_W_FM_FREF_VALUE 0LL /* 50k steps, 150k IF */
+
+#define TEA5777_W_FM_FORCEMONO_MASK (1LL << 15)
+#define TEA5777_W_FM_FORCEMONO_SHIFT 15
+#define TEA5777_W_FM_SDSOFF_MASK (1LL << 14)
+#define TEA5777_W_FM_SDSOFF_SHIFT 14
+#define TEA5777_W_FM_DOFF_MASK (1LL << 13)
+#define TEA5777_W_FM_DOFF_SHIFT 13
+
+#define TEA5777_W_FM_STEP_MASK (3LL << 1)
+#define TEA5777_W_FM_STEP_SHIFT 1
+
+/* Write reg, AM specific bits */
+#define TEA5777_W_AM_PLL_MASK (0x7ffLL << 34)
+#define TEA5777_W_AM_PLL_SHIFT 34
+#define TEA5777_W_AM_AGCRF_MASK (1LL << 33)
+#define TEA5777_W_AM_AGCRF_SHIFT 33
+#define TEA5777_W_AM_AGCIF_MASK (1LL << 32)
+#define TEA5777_W_AM_AGCIF_SHIFT 32
+#define TEA5777_W_AM_MWLW_MASK (1LL << 31)
+#define TEA5777_W_AM_MWLW_SHIFT 31
+#define TEA5777_W_AM_LW 0LL
+#define TEA5777_W_AM_MW 1LL
+#define TEA5777_W_AM_LNA_MASK (1LL << 30)
+#define TEA5777_W_AM_LNA_SHIFT 30
+
+#define TEA5777_W_AM_PEAK_MASK (1LL << 25)
+#define TEA5777_W_AM_PEAK_SHIFT 25
+
+#define TEA5777_W_AM_RFB_MASK (1LL << 16)
+#define TEA5777_W_AM_RFB_SHIFT 16
+#define TEA5777_W_AM_CALLIGN_MASK (1LL << 15)
+#define TEA5777_W_AM_CALLIGN_SHIFT 15
+#define TEA5777_W_AM_CBANK_MASK (0x7fLL << 8)
+#define TEA5777_W_AM_CBANK_SHIFT 8
+
+#define TEA5777_W_AM_DELAY_MASK (1LL << 2)
+#define TEA5777_W_AM_DELAY_SHIFT 2
+#define TEA5777_W_AM_STEP_MASK (1LL << 1)
+#define TEA5777_W_AM_STEP_SHIFT 1
+
+/* Read reg, common bits */
+#define TEA5777_R_LEVEL_MASK (0x0f << 17)
+#define TEA5777_R_LEVEL_SHIFT 17
+#define TEA5777_R_SFOUND_MASK (0x01 << 16)
+#define TEA5777_R_SFOUND_SHIFT 16
+#define TEA5777_R_BLIM_MASK (0x01 << 15)
+#define TEA5777_R_BLIM_SHIFT 15
+
+/* Read reg, FM specific bits */
+#define TEA5777_R_FM_STEREO_MASK (0x01 << 21)
+#define TEA5777_R_FM_STEREO_SHIFT 21
+#define TEA5777_R_FM_PLL_MASK 0x1fff
+#define TEA5777_R_FM_PLL_SHIFT 0
+
+enum { BAND_FM, BAND_AM };
+
+static const struct v4l2_frequency_band bands[] = {
+ {
+ .type = V4L2_TUNER_RADIO,
+ .index = 0,
+ .capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO |
+ V4L2_TUNER_CAP_FREQ_BANDS |
+ V4L2_TUNER_CAP_HWSEEK_BOUNDED |
+ V4L2_TUNER_CAP_HWSEEK_PROG_LIM,
+ .rangelow = 76000 * 16,
+ .rangehigh = 108000 * 16,
+ .modulation = V4L2_BAND_MODULATION_FM,
+ },
+ {
+ .type = V4L2_TUNER_RADIO,
+ .index = 1,
+ .capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_FREQ_BANDS |
+ V4L2_TUNER_CAP_HWSEEK_BOUNDED |
+ V4L2_TUNER_CAP_HWSEEK_PROG_LIM,
+ .rangelow = 530 * 16,
+ .rangehigh = 1710 * 16,
+ .modulation = V4L2_BAND_MODULATION_AM,
+ },
+};
+
+static u32 tea5777_freq_to_v4l2_freq(struct radio_tea5777 *tea, u32 freq)
+{
+ switch (tea->band) {
+ case BAND_FM:
+ return (freq * TEA5777_FM_FREQ_STEP + TEA5777_FM_IF) * 16;
+ case BAND_AM:
+ return (freq * TEA5777_AM_FREQ_STEP + TEA5777_AM_IF) * 16;
+ }
+ return 0; /* Never reached */
+}
+
+int radio_tea5777_set_freq(struct radio_tea5777 *tea)
+{
+ u32 freq;
+ int res;
+
+ freq = clamp(tea->freq, bands[tea->band].rangelow,
+ bands[tea->band].rangehigh);
+ freq = (freq + 8) / 16; /* to kHz */
+
+ switch (tea->band) {
+ case BAND_FM:
+ tea->write_reg &= ~TEA5777_W_AM_FM_MASK;
+ freq = (freq - TEA5777_FM_IF) / TEA5777_FM_FREQ_STEP;
+ tea->write_reg &= ~TEA5777_W_FM_PLL_MASK;
+ tea->write_reg |= (u64)freq << TEA5777_W_FM_PLL_SHIFT;
+ tea->write_reg &= ~TEA5777_W_FM_FREF_MASK;
+ tea->write_reg |= TEA5777_W_FM_FREF_VALUE <<
+ TEA5777_W_FM_FREF_SHIFT;
+ tea->write_reg &= ~TEA5777_W_FM_FORCEMONO_MASK;
+ if (tea->audmode == V4L2_TUNER_MODE_MONO)
+ tea->write_reg |= 1LL << TEA5777_W_FM_FORCEMONO_SHIFT;
+ break;
+ case BAND_AM:
+ tea->write_reg &= ~TEA5777_W_AM_FM_MASK;
+ tea->write_reg |= (1LL << TEA5777_W_AM_FM_SHIFT);
+ freq = (freq - TEA5777_AM_IF) / TEA5777_AM_FREQ_STEP;
+ tea->write_reg &= ~TEA5777_W_AM_PLL_MASK;
+ tea->write_reg |= (u64)freq << TEA5777_W_AM_PLL_SHIFT;
+ tea->write_reg &= ~TEA5777_W_AM_AGCRF_MASK;
+ tea->write_reg &= ~TEA5777_W_AM_AGCRF_MASK;
+ tea->write_reg &= ~TEA5777_W_AM_MWLW_MASK;
+ tea->write_reg |= TEA5777_W_AM_MW << TEA5777_W_AM_MWLW_SHIFT;
+ tea->write_reg &= ~TEA5777_W_AM_LNA_MASK;
+ tea->write_reg |= 1LL << TEA5777_W_AM_LNA_SHIFT;
+ tea->write_reg &= ~TEA5777_W_AM_PEAK_MASK;
+ tea->write_reg |= 1LL << TEA5777_W_AM_PEAK_SHIFT;
+ tea->write_reg &= ~TEA5777_W_AM_CALLIGN_MASK;
+ break;
+ }
+
+ res = tea->ops->write_reg(tea, tea->write_reg);
+ if (res)
+ return res;
+
+ tea->needs_write = false;
+ tea->read_reg = -1;
+ tea->freq = tea5777_freq_to_v4l2_freq(tea, freq);
+
+ return 0;
+}
+
+static int radio_tea5777_update_read_reg(struct radio_tea5777 *tea, int wait)
+{
+ int res;
+
+ if (tea->read_reg != -1)
+ return 0;
+
+ if (tea->write_before_read && tea->needs_write) {
+ res = radio_tea5777_set_freq(tea);
+ if (res)
+ return res;
+ }
+
+ if (wait) {
+ if (schedule_timeout_interruptible(msecs_to_jiffies(wait)))
+ return -ERESTARTSYS;
+ }
+
+ res = tea->ops->read_reg(tea, &tea->read_reg);
+ if (res)
+ return res;
+
+ tea->needs_write = true;
+ return 0;
+}
+
+/*
+ * Linux Video interface
+ */
+
+static int vidioc_querycap(struct file *file, void *priv,
+ struct v4l2_capability *v)
+{
+ struct radio_tea5777 *tea = video_drvdata(file);
+
+ strscpy(v->driver, tea->v4l2_dev->name, sizeof(v->driver));
+ strscpy(v->card, tea->card, sizeof(v->card));
+ strlcat(v->card, " TEA5777", sizeof(v->card));
+ strscpy(v->bus_info, tea->bus_info, sizeof(v->bus_info));
+ return 0;
+}
+
+static int vidioc_enum_freq_bands(struct file *file, void *priv,
+ struct v4l2_frequency_band *band)
+{
+ struct radio_tea5777 *tea = video_drvdata(file);
+
+ if (band->tuner != 0 || band->index >= ARRAY_SIZE(bands) ||
+ (!tea->has_am && band->index == BAND_AM))
+ return -EINVAL;
+
+ *band = bands[band->index];
+ return 0;
+}
+
+static int vidioc_g_tuner(struct file *file, void *priv,
+ struct v4l2_tuner *v)
+{
+ struct radio_tea5777 *tea = video_drvdata(file);
+ int res;
+
+ if (v->index > 0)
+ return -EINVAL;
+
+ res = radio_tea5777_update_read_reg(tea, 0);
+ if (res)
+ return res;
+
+ memset(v, 0, sizeof(*v));
+ if (tea->has_am)
+ strscpy(v->name, "AM/FM", sizeof(v->name));
+ else
+ strscpy(v->name, "FM", sizeof(v->name));
+ v->type = V4L2_TUNER_RADIO;
+ v->capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO |
+ V4L2_TUNER_CAP_FREQ_BANDS |
+ V4L2_TUNER_CAP_HWSEEK_BOUNDED |
+ V4L2_TUNER_CAP_HWSEEK_PROG_LIM;
+ v->rangelow = tea->has_am ? bands[BAND_AM].rangelow :
+ bands[BAND_FM].rangelow;
+ v->rangehigh = bands[BAND_FM].rangehigh;
+ if (tea->band == BAND_FM &&
+ (tea->read_reg & TEA5777_R_FM_STEREO_MASK))
+ v->rxsubchans = V4L2_TUNER_SUB_STEREO;
+ else
+ v->rxsubchans = V4L2_TUNER_SUB_MONO;
+ v->audmode = tea->audmode;
+ /* shift - 12 to convert 4-bits (0-15) scale to 16-bits (0-65535) */
+ v->signal = (tea->read_reg & TEA5777_R_LEVEL_MASK) >>
+ (TEA5777_R_LEVEL_SHIFT - 12);
+
+ /* Invalidate read_reg, so that next call we return up2date signal */
+ tea->read_reg = -1;
+
+ return 0;
+}
+
+static int vidioc_s_tuner(struct file *file, void *priv,
+ const struct v4l2_tuner *v)
+{
+ struct radio_tea5777 *tea = video_drvdata(file);
+ u32 orig_audmode = tea->audmode;
+
+ if (v->index)
+ return -EINVAL;
+
+ tea->audmode = v->audmode;
+ if (tea->audmode > V4L2_TUNER_MODE_STEREO)
+ tea->audmode = V4L2_TUNER_MODE_STEREO;
+
+ if (tea->audmode != orig_audmode && tea->band == BAND_FM)
+ return radio_tea5777_set_freq(tea);
+
+ return 0;
+}
+
+static int vidioc_g_frequency(struct file *file, void *priv,
+ struct v4l2_frequency *f)
+{
+ struct radio_tea5777 *tea = video_drvdata(file);
+
+ if (f->tuner != 0)
+ return -EINVAL;
+ f->type = V4L2_TUNER_RADIO;
+ f->frequency = tea->freq;
+ return 0;
+}
+
+static int vidioc_s_frequency(struct file *file, void *priv,
+ const struct v4l2_frequency *f)
+{
+ struct radio_tea5777 *tea = video_drvdata(file);
+
+ if (f->tuner != 0 || f->type != V4L2_TUNER_RADIO)
+ return -EINVAL;
+
+ if (tea->has_am && f->frequency < (20000 * 16))
+ tea->band = BAND_AM;
+ else
+ tea->band = BAND_FM;
+
+ tea->freq = f->frequency;
+ return radio_tea5777_set_freq(tea);
+}
+
+static int vidioc_s_hw_freq_seek(struct file *file, void *fh,
+ const struct v4l2_hw_freq_seek *a)
+{
+ struct radio_tea5777 *tea = video_drvdata(file);
+ unsigned long timeout;
+ u32 rangelow = a->rangelow;
+ u32 rangehigh = a->rangehigh;
+ int i, res, spacing;
+ u32 orig_freq;
+
+ if (a->tuner || a->wrap_around)
+ return -EINVAL;
+
+ if (file->f_flags & O_NONBLOCK)
+ return -EWOULDBLOCK;
+
+ if (rangelow || rangehigh) {
+ for (i = 0; i < ARRAY_SIZE(bands); i++) {
+ if (i == BAND_AM && !tea->has_am)
+ continue;
+ if (bands[i].rangelow >= rangelow &&
+ bands[i].rangehigh <= rangehigh)
+ break;
+ }
+ if (i == ARRAY_SIZE(bands))
+ return -EINVAL; /* No matching band found */
+
+ tea->band = i;
+ if (tea->freq < rangelow || tea->freq > rangehigh) {
+ tea->freq = clamp(tea->freq, rangelow,
+ rangehigh);
+ res = radio_tea5777_set_freq(tea);
+ if (res)
+ return res;
+ }
+ } else {
+ rangelow = bands[tea->band].rangelow;
+ rangehigh = bands[tea->band].rangehigh;
+ }
+
+ spacing = (tea->band == BAND_AM) ? (5 * 16) : (200 * 16); /* kHz */
+ orig_freq = tea->freq;
+
+ tea->write_reg |= TEA5777_W_PROGBLIM_MASK;
+ if (tea->seek_rangelow != rangelow) {
+ tea->write_reg &= ~TEA5777_W_UPDWN_MASK;
+ tea->freq = rangelow;
+ res = radio_tea5777_set_freq(tea);
+ if (res)
+ goto leave;
+ tea->seek_rangelow = rangelow;
+ }
+ if (tea->seek_rangehigh != rangehigh) {
+ tea->write_reg |= TEA5777_W_UPDWN_MASK;
+ tea->freq = rangehigh;
+ res = radio_tea5777_set_freq(tea);
+ if (res)
+ goto leave;
+ tea->seek_rangehigh = rangehigh;
+ }
+ tea->write_reg &= ~TEA5777_W_PROGBLIM_MASK;
+
+ tea->write_reg |= TEA5777_W_SEARCH_MASK;
+ if (a->seek_upward) {
+ tea->write_reg |= TEA5777_W_UPDWN_MASK;
+ tea->freq = orig_freq + spacing;
+ } else {
+ tea->write_reg &= ~TEA5777_W_UPDWN_MASK;
+ tea->freq = orig_freq - spacing;
+ }
+ res = radio_tea5777_set_freq(tea);
+ if (res)
+ goto leave;
+
+ timeout = jiffies + msecs_to_jiffies(5000);
+ for (;;) {
+ if (time_after(jiffies, timeout)) {
+ res = -ENODATA;
+ break;
+ }
+
+ res = radio_tea5777_update_read_reg(tea, 100);
+ if (res)
+ break;
+
+ /*
+ * Note we use tea->freq to track how far we've searched sofar
+ * this is necessary to ensure we continue seeking at the right
+ * point, in the write_before_read case.
+ */
+ tea->freq = (tea->read_reg & TEA5777_R_FM_PLL_MASK);
+ tea->freq = tea5777_freq_to_v4l2_freq(tea, tea->freq);
+
+ if ((tea->read_reg & TEA5777_R_SFOUND_MASK)) {
+ tea->write_reg &= ~TEA5777_W_SEARCH_MASK;
+ return 0;
+ }
+
+ if (tea->read_reg & TEA5777_R_BLIM_MASK) {
+ res = -ENODATA;
+ break;
+ }
+
+ /* Force read_reg update */
+ tea->read_reg = -1;
+ }
+leave:
+ tea->write_reg &= ~TEA5777_W_PROGBLIM_MASK;
+ tea->write_reg &= ~TEA5777_W_SEARCH_MASK;
+ tea->freq = orig_freq;
+ radio_tea5777_set_freq(tea);
+ return res;
+}
+
+static int tea575x_s_ctrl(struct v4l2_ctrl *c)
+{
+ struct radio_tea5777 *tea =
+ container_of(c->handler, struct radio_tea5777, ctrl_handler);
+
+ switch (c->id) {
+ case V4L2_CID_AUDIO_MUTE:
+ if (c->val)
+ tea->write_reg |= TEA5777_W_MUTE_MASK;
+ else
+ tea->write_reg &= ~TEA5777_W_MUTE_MASK;
+
+ return radio_tea5777_set_freq(tea);
+ }
+
+ return -EINVAL;
+}
+
+static const struct v4l2_file_operations tea575x_fops = {
+ .unlocked_ioctl = video_ioctl2,
+ .open = v4l2_fh_open,
+ .release = v4l2_fh_release,
+ .poll = v4l2_ctrl_poll,
+};
+
+static const struct v4l2_ioctl_ops tea575x_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_s_hw_freq_seek = vidioc_s_hw_freq_seek,
+ .vidioc_enum_freq_bands = vidioc_enum_freq_bands,
+ .vidioc_log_status = v4l2_ctrl_log_status,
+ .vidioc_subscribe_event = v4l2_ctrl_subscribe_event,
+ .vidioc_unsubscribe_event = v4l2_event_unsubscribe,
+};
+
+static const struct video_device tea575x_radio = {
+ .ioctl_ops = &tea575x_ioctl_ops,
+ .release = video_device_release_empty,
+};
+
+static const struct v4l2_ctrl_ops tea575x_ctrl_ops = {
+ .s_ctrl = tea575x_s_ctrl,
+};
+
+int radio_tea5777_init(struct radio_tea5777 *tea, struct module *owner)
+{
+ int res;
+
+ tea->write_reg = (1LL << TEA5777_W_IFCE_SHIFT) |
+ (1LL << TEA5777_W_IFW_SHIFT) |
+ (1LL << TEA5777_W_INTEXT_SHIFT) |
+ (1LL << TEA5777_W_CHP0_SHIFT) |
+ (1LL << TEA5777_W_SLEV_SHIFT);
+ tea->freq = 90500 * 16; /* 90.5Mhz default */
+ tea->audmode = V4L2_TUNER_MODE_STEREO;
+ res = radio_tea5777_set_freq(tea);
+ if (res) {
+ v4l2_err(tea->v4l2_dev, "can't set initial freq (%d)\n", res);
+ return res;
+ }
+
+ tea->vd = tea575x_radio;
+ video_set_drvdata(&tea->vd, tea);
+ mutex_init(&tea->mutex);
+ strscpy(tea->vd.name, tea->v4l2_dev->name, sizeof(tea->vd.name));
+ tea->vd.lock = &tea->mutex;
+ tea->vd.v4l2_dev = tea->v4l2_dev;
+ tea->vd.device_caps = V4L2_CAP_TUNER | V4L2_CAP_RADIO |
+ V4L2_CAP_HW_FREQ_SEEK;
+ tea->fops = tea575x_fops;
+ tea->fops.owner = owner;
+ tea->vd.fops = &tea->fops;
+
+ tea->vd.ctrl_handler = &tea->ctrl_handler;
+ v4l2_ctrl_handler_init(&tea->ctrl_handler, 1);
+ v4l2_ctrl_new_std(&tea->ctrl_handler, &tea575x_ctrl_ops,
+ V4L2_CID_AUDIO_MUTE, 0, 1, 1, 1);
+ res = tea->ctrl_handler.error;
+ if (res) {
+ v4l2_err(tea->v4l2_dev, "can't initialize controls\n");
+ v4l2_ctrl_handler_free(&tea->ctrl_handler);
+ return res;
+ }
+ v4l2_ctrl_handler_setup(&tea->ctrl_handler);
+
+ res = video_register_device(&tea->vd, VFL_TYPE_RADIO, -1);
+ if (res) {
+ v4l2_err(tea->v4l2_dev, "can't register video device!\n");
+ v4l2_ctrl_handler_free(tea->vd.ctrl_handler);
+ return res;
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(radio_tea5777_init);
+
+void radio_tea5777_exit(struct radio_tea5777 *tea)
+{
+ video_unregister_device(&tea->vd);
+ v4l2_ctrl_handler_free(tea->vd.ctrl_handler);
+}
+EXPORT_SYMBOL_GPL(radio_tea5777_exit);
diff --git a/drivers/media/radio/radio-tea5777.h b/drivers/media/radio/radio-tea5777.h
new file mode 100644
index 000000000..33470a046
--- /dev/null
+++ b/drivers/media/radio/radio-tea5777.h
@@ -0,0 +1,76 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#ifndef __RADIO_TEA5777_H
+#define __RADIO_TEA5777_H
+
+/*
+ * v4l2 driver for TEA5777 Philips AM/FM radio tuner chips
+ *
+ * Copyright (c) 2012 Hans de Goede <hdegoede@redhat.com>
+ *
+ * Based on the ALSA driver for TEA5757/5759 Philips AM/FM radio tuner chips:
+ *
+ * Copyright (c) 2004 Jaroslav Kysela <perex@perex.cz>
+ * Copyright (c) 2012 Hans de Goede <hdegoede@redhat.com>
+ */
+
+#include <linux/videodev2.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-dev.h>
+#include <media/v4l2-device.h>
+
+#define TEA575X_FMIF 10700
+#define TEA575X_AMIF 450
+
+struct radio_tea5777;
+
+struct radio_tea5777_ops {
+ /*
+ * Write the 6 bytes large write register of the tea5777
+ *
+ * val represents the 6 write registers, with byte 1 from the
+ * datasheet being the most significant byte (so byte 5 of the u64),
+ * and byte 6 from the datasheet being the least significant byte.
+ *
+ * returns 0 on success.
+ */
+ int (*write_reg)(struct radio_tea5777 *tea, u64 val);
+ /*
+ * Read the 3 bytes large read register of the tea5777
+ *
+ * The read value gets returned in val, akin to write_reg, byte 1 from
+ * the datasheet is stored as the most significant byte (so byte 2 of
+ * the u32), and byte 3 from the datasheet gets stored as the least
+ * significant byte (iow byte 0 of the u32).
+ *
+ * returns 0 on success.
+ */
+ int (*read_reg)(struct radio_tea5777 *tea, u32 *val);
+};
+
+struct radio_tea5777 {
+ struct v4l2_device *v4l2_dev;
+ struct v4l2_file_operations fops;
+ struct video_device vd; /* video device */
+ bool has_am; /* Device can tune to AM freqs */
+ bool write_before_read; /* must write before read quirk */
+ bool needs_write; /* for write before read quirk */
+ u32 band; /* current band */
+ u32 freq; /* current frequency */
+ u32 audmode; /* last set audmode */
+ u32 seek_rangelow; /* current hwseek limits */
+ u32 seek_rangehigh;
+ u32 read_reg;
+ u64 write_reg;
+ struct mutex mutex;
+ const struct radio_tea5777_ops *ops;
+ void *private_data;
+ u8 card[32];
+ u8 bus_info[32];
+ struct v4l2_ctrl_handler ctrl_handler;
+};
+
+int radio_tea5777_init(struct radio_tea5777 *tea, struct module *owner);
+void radio_tea5777_exit(struct radio_tea5777 *tea);
+int radio_tea5777_set_freq(struct radio_tea5777 *tea);
+
+#endif /* __RADIO_TEA5777_H */
diff --git a/drivers/media/radio/radio-terratec.c b/drivers/media/radio/radio-terratec.c
new file mode 100644
index 000000000..9231a19d5
--- /dev/null
+++ b/drivers/media/radio/radio-terratec.c
@@ -0,0 +1,168 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Terratec ActiveRadio ISA Standalone card driver for Linux radio support
+ * (c) 1999 R. Offermanns (rolf@offermanns.de)
+ * based on the aimslab radio driver from M. Kirkwood
+ * many thanks to Michael Becker and Friedhelm Birth (from TerraTec)
+ *
+ *
+ * History:
+ * 1999-05-21 First preview release
+ *
+ * Notes on the hardware:
+ * There are two "main" chips on the card:
+ * - Philips OM5610 (http://www-us.semiconductors.philips.com/acrobat/datasheets/OM5610_2.pdf)
+ * - Philips SAA6588 (http://www-us.semiconductors.philips.com/acrobat/datasheets/SAA6588_1.pdf)
+ * (you can get the datasheet at the above links)
+ *
+ * Frequency control is done digitally -- ie out(port,encodefreq(95.8));
+ * Volume Control is done digitally
+ *
+ * Converted to the radio-isa framework by Hans Verkuil <hverkuil@kernel.org>
+ * Converted to V4L2 API by Mauro Carvalho Chehab <mchehab@kernel.org>
+ */
+
+#include <linux/module.h> /* Modules */
+#include <linux/init.h> /* Initdata */
+#include <linux/ioport.h> /* request_region */
+#include <linux/videodev2.h> /* kernel radio structs */
+#include <linux/mutex.h>
+#include <linux/io.h> /* outb, outb_p */
+#include <linux/slab.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-ioctl.h>
+#include "radio-isa.h"
+
+MODULE_AUTHOR("R. Offermans & others");
+MODULE_DESCRIPTION("A driver for the TerraTec ActiveRadio Standalone radio card.");
+MODULE_LICENSE("GPL");
+MODULE_VERSION("0.1.99");
+
+/* Note: there seems to be only one possible port (0x590), but without
+ hardware this is hard to verify. For now, this is the only one we will
+ support. */
+static int io = 0x590;
+static int radio_nr = -1;
+
+module_param(radio_nr, int, 0444);
+MODULE_PARM_DESC(radio_nr, "Radio device number");
+
+#define WRT_DIS 0x00
+#define CLK_OFF 0x00
+#define IIC_DATA 0x01
+#define IIC_CLK 0x02
+#define DATA 0x04
+#define CLK_ON 0x08
+#define WRT_EN 0x10
+
+static struct radio_isa_card *terratec_alloc(void)
+{
+ return kzalloc_obj(struct radio_isa_card);
+}
+
+static int terratec_s_mute_volume(struct radio_isa_card *isa, bool mute, int vol)
+{
+ int i;
+
+ if (mute)
+ vol = 0;
+ vol = vol + (vol * 32); /* change both channels */
+ for (i = 0; i < 8; i++) {
+ if (vol & (0x80 >> i))
+ outb(0x80, isa->io + 1);
+ else
+ outb(0x00, isa->io + 1);
+ }
+ return 0;
+}
+
+
+/* this is the worst part in this driver */
+/* many more or less strange things are going on here, but hey, it works :) */
+
+static int terratec_s_frequency(struct radio_isa_card *isa, u32 freq)
+{
+ int i;
+ int temp;
+ long rest;
+ unsigned char buffer[25]; /* we have to bit shift 25 registers */
+
+ freq = freq / 160; /* convert the freq. to a nice to handle value */
+ memset(buffer, 0, sizeof(buffer));
+
+ rest = freq * 10 + 10700; /* I once had understood what is going on here */
+ /* maybe some wise guy (friedhelm?) can comment this stuff */
+ i = 13;
+ temp = 102400;
+ while (rest != 0) {
+ if (rest % temp == rest)
+ buffer[i] = 0;
+ else {
+ buffer[i] = 1;
+ rest = rest - temp;
+ }
+ i--;
+ temp = temp / 2;
+ }
+
+ for (i = 24; i > -1; i--) { /* bit shift the values to the radiocard */
+ if (buffer[i] == 1) {
+ outb(WRT_EN | DATA, isa->io);
+ outb(WRT_EN | DATA | CLK_ON, isa->io);
+ outb(WRT_EN | DATA, isa->io);
+ } else {
+ outb(WRT_EN | 0x00, isa->io);
+ outb(WRT_EN | 0x00 | CLK_ON, isa->io);
+ }
+ }
+ outb(0x00, isa->io);
+ return 0;
+}
+
+static u32 terratec_g_signal(struct radio_isa_card *isa)
+{
+ /* bit set = no signal present */
+ return (inb(isa->io) & 2) ? 0 : 0xffff;
+}
+
+static const struct radio_isa_ops terratec_ops = {
+ .alloc = terratec_alloc,
+ .s_mute_volume = terratec_s_mute_volume,
+ .s_frequency = terratec_s_frequency,
+ .g_signal = terratec_g_signal,
+};
+
+static const int terratec_ioports[] = { 0x590 };
+
+static struct radio_isa_driver terratec_driver = {
+ .driver = {
+ .match = radio_isa_match,
+ .probe = radio_isa_probe,
+ .remove = radio_isa_remove,
+ .driver = {
+ .name = "radio-terratec",
+ },
+ },
+ .io_params = &io,
+ .radio_nr_params = &radio_nr,
+ .io_ports = terratec_ioports,
+ .num_of_io_ports = ARRAY_SIZE(terratec_ioports),
+ .region_size = 2,
+ .card = "TerraTec ActiveRadio",
+ .ops = &terratec_ops,
+ .has_stereo = true,
+ .max_volume = 10,
+};
+
+static int __init terratec_init(void)
+{
+ return isa_register_driver(&terratec_driver.driver, 1);
+}
+
+static void __exit terratec_exit(void)
+{
+ isa_unregister_driver(&terratec_driver.driver);
+}
+
+module_init(terratec_init);
+module_exit(terratec_exit);
+
diff --git a/drivers/media/radio/radio-timb.c b/drivers/media/radio/radio-timb.c
new file mode 100644
index 000000000..a6069b106
--- /dev/null
+++ b/drivers/media/radio/radio-timb.c
@@ -0,0 +1,176 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * radio-timb.c Timberdale FPGA Radio driver
+ * Copyright (c) 2009 Intel Corporation
+ */
+
+#include <linux/io.h>
+#include <media/v4l2-ioctl.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-event.h>
+#include <linux/platform_device.h>
+#include <linux/interrupt.h>
+#include <linux/slab.h>
+#include <linux/i2c.h>
+#include <linux/module.h>
+#include <linux/platform_data/media/timb_radio.h>
+
+#define DRIVER_NAME "timb-radio"
+
+struct timbradio {
+ struct timb_radio_platform_data pdata;
+ struct v4l2_subdev *sd_tuner;
+ struct v4l2_subdev *sd_dsp;
+ struct video_device video_dev;
+ struct v4l2_device v4l2_dev;
+ struct mutex lock;
+};
+
+
+static int timbradio_vidioc_querycap(struct file *file, void *priv,
+ struct v4l2_capability *v)
+{
+ strscpy(v->driver, DRIVER_NAME, sizeof(v->driver));
+ strscpy(v->card, "Timberdale Radio", sizeof(v->card));
+ snprintf(v->bus_info, sizeof(v->bus_info), "platform:"DRIVER_NAME);
+ return 0;
+}
+
+static int timbradio_vidioc_g_tuner(struct file *file, void *priv,
+ struct v4l2_tuner *v)
+{
+ struct timbradio *tr = video_drvdata(file);
+ return v4l2_subdev_call(tr->sd_tuner, tuner, g_tuner, v);
+}
+
+static int timbradio_vidioc_s_tuner(struct file *file, void *priv,
+ const struct v4l2_tuner *v)
+{
+ struct timbradio *tr = video_drvdata(file);
+ return v4l2_subdev_call(tr->sd_tuner, tuner, s_tuner, v);
+}
+
+static int timbradio_vidioc_s_frequency(struct file *file, void *priv,
+ const struct v4l2_frequency *f)
+{
+ struct timbradio *tr = video_drvdata(file);
+ return v4l2_subdev_call(tr->sd_tuner, tuner, s_frequency, f);
+}
+
+static int timbradio_vidioc_g_frequency(struct file *file, void *priv,
+ struct v4l2_frequency *f)
+{
+ struct timbradio *tr = video_drvdata(file);
+ return v4l2_subdev_call(tr->sd_tuner, tuner, g_frequency, f);
+}
+
+static const struct v4l2_ioctl_ops timbradio_ioctl_ops = {
+ .vidioc_querycap = timbradio_vidioc_querycap,
+ .vidioc_g_tuner = timbradio_vidioc_g_tuner,
+ .vidioc_s_tuner = timbradio_vidioc_s_tuner,
+ .vidioc_g_frequency = timbradio_vidioc_g_frequency,
+ .vidioc_s_frequency = timbradio_vidioc_s_frequency,
+ .vidioc_log_status = v4l2_ctrl_log_status,
+ .vidioc_subscribe_event = v4l2_ctrl_subscribe_event,
+ .vidioc_unsubscribe_event = v4l2_event_unsubscribe,
+};
+
+static const struct v4l2_file_operations timbradio_fops = {
+ .owner = THIS_MODULE,
+ .open = v4l2_fh_open,
+ .release = v4l2_fh_release,
+ .poll = v4l2_ctrl_poll,
+ .unlocked_ioctl = video_ioctl2,
+};
+
+static int timbradio_probe(struct platform_device *pdev)
+{
+ struct timb_radio_platform_data *pdata = pdev->dev.platform_data;
+ struct timbradio *tr;
+ int err;
+
+ if (!pdata) {
+ dev_err(&pdev->dev, "Platform data missing\n");
+ err = -EINVAL;
+ goto err;
+ }
+
+ tr = devm_kzalloc(&pdev->dev, sizeof(*tr), GFP_KERNEL);
+ if (!tr) {
+ err = -ENOMEM;
+ goto err;
+ }
+
+ tr->pdata = *pdata;
+ mutex_init(&tr->lock);
+
+ strscpy(tr->video_dev.name, "Timberdale Radio",
+ sizeof(tr->video_dev.name));
+ tr->video_dev.fops = &timbradio_fops;
+ tr->video_dev.ioctl_ops = &timbradio_ioctl_ops;
+ tr->video_dev.release = video_device_release_empty;
+ tr->video_dev.minor = -1;
+ tr->video_dev.lock = &tr->lock;
+ tr->video_dev.device_caps = V4L2_CAP_TUNER | V4L2_CAP_RADIO;
+
+ strscpy(tr->v4l2_dev.name, DRIVER_NAME, sizeof(tr->v4l2_dev.name));
+ err = v4l2_device_register(NULL, &tr->v4l2_dev);
+ if (err)
+ goto err;
+
+ tr->video_dev.v4l2_dev = &tr->v4l2_dev;
+
+ tr->sd_tuner = v4l2_i2c_new_subdev_board(&tr->v4l2_dev,
+ i2c_get_adapter(pdata->i2c_adapter), pdata->tuner, NULL);
+ tr->sd_dsp = v4l2_i2c_new_subdev_board(&tr->v4l2_dev,
+ i2c_get_adapter(pdata->i2c_adapter), pdata->dsp, NULL);
+ if (tr->sd_tuner == NULL || tr->sd_dsp == NULL) {
+ err = -ENODEV;
+ goto err_video_req;
+ }
+
+ tr->v4l2_dev.ctrl_handler = tr->sd_dsp->ctrl_handler;
+
+ err = video_register_device(&tr->video_dev, VFL_TYPE_RADIO, -1);
+ if (err) {
+ dev_err(&pdev->dev, "Error reg video\n");
+ goto err_video_req;
+ }
+
+ video_set_drvdata(&tr->video_dev, tr);
+
+ platform_set_drvdata(pdev, tr);
+ return 0;
+
+err_video_req:
+ v4l2_device_unregister(&tr->v4l2_dev);
+err:
+ dev_err(&pdev->dev, "Failed to register: %d\n", err);
+
+ return err;
+}
+
+static void timbradio_remove(struct platform_device *pdev)
+{
+ struct timbradio *tr = platform_get_drvdata(pdev);
+
+ video_unregister_device(&tr->video_dev);
+ v4l2_device_unregister(&tr->v4l2_dev);
+}
+
+static struct platform_driver timbradio_platform_driver = {
+ .driver = {
+ .name = DRIVER_NAME,
+ },
+ .probe = timbradio_probe,
+ .remove = timbradio_remove,
+};
+
+module_platform_driver(timbradio_platform_driver);
+
+MODULE_DESCRIPTION("Timberdale Radio driver");
+MODULE_AUTHOR("Mocean Laboratories <info@mocean-labs.com>");
+MODULE_LICENSE("GPL v2");
+MODULE_VERSION("0.0.2");
+MODULE_ALIAS("platform:"DRIVER_NAME);
diff --git a/drivers/media/radio/radio-trust.c b/drivers/media/radio/radio-trust.c
new file mode 100644
index 000000000..c7b30d430
--- /dev/null
+++ b/drivers/media/radio/radio-trust.c
@@ -0,0 +1,243 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* radio-trust.c - Trust FM Radio card driver for Linux 2.2
+ * by Eric Lammerts <eric@scintilla.utwente.nl>
+ *
+ * Based on radio-aztech.c. Original notes:
+ *
+ * Adapted to support the Video for Linux API by
+ * Russell Kroll <rkroll@exploits.org>. Based on original tuner code by:
+ *
+ * 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)
+ *
+ * Converted to V4L2 API by Mauro Carvalho Chehab <mchehab@kernel.org>
+ */
+
+#include <linux/module.h>
+#include <linux/init.h>
+#include <linux/ioport.h>
+#include <linux/videodev2.h>
+#include <linux/io.h>
+#include <linux/slab.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-ioctl.h>
+#include "radio-isa.h"
+
+MODULE_AUTHOR("Eric Lammerts, Russell Kroll, Quay Lu, Donald Song, Jason Lewis, Scott McGrath, William McGrath");
+MODULE_DESCRIPTION("A driver for the Trust FM Radio card.");
+MODULE_LICENSE("GPL");
+MODULE_VERSION("0.1.99");
+
+/* acceptable ports: 0x350 (JP3 shorted), 0x358 (JP3 open) */
+
+#ifndef CONFIG_RADIO_TRUST_PORT
+#define CONFIG_RADIO_TRUST_PORT -1
+#endif
+
+#define TRUST_MAX 2
+
+static int io[TRUST_MAX] = { [0] = CONFIG_RADIO_TRUST_PORT,
+ [1 ... (TRUST_MAX - 1)] = -1 };
+static int radio_nr[TRUST_MAX] = { [0 ... (TRUST_MAX - 1)] = -1 };
+
+module_param_array(io, int, NULL, 0444);
+MODULE_PARM_DESC(io, "I/O addresses of the Trust FM Radio card (0x350 or 0x358)");
+module_param_array(radio_nr, int, NULL, 0444);
+MODULE_PARM_DESC(radio_nr, "Radio device numbers");
+
+struct trust {
+ struct radio_isa_card isa;
+ int ioval;
+};
+
+static struct radio_isa_card *trust_alloc(void)
+{
+ struct trust *tr = kzalloc_obj(*tr);
+
+ return tr ? &tr->isa : NULL;
+}
+
+/* i2c addresses */
+#define TDA7318_ADDR 0x88
+#define TSA6060T_ADDR 0xc4
+
+#define TR_DELAY do { inb(tr->isa.io); inb(tr->isa.io); inb(tr->isa.io); } while (0)
+#define TR_SET_SCL outb(tr->ioval |= 2, tr->isa.io)
+#define TR_CLR_SCL outb(tr->ioval &= 0xfd, tr->isa.io)
+#define TR_SET_SDA outb(tr->ioval |= 1, tr->isa.io)
+#define TR_CLR_SDA outb(tr->ioval &= 0xfe, tr->isa.io)
+
+static void write_i2c(struct trust *tr, int n, ...)
+{
+ unsigned char val, mask;
+ va_list args;
+
+ va_start(args, n);
+
+ /* start condition */
+ TR_SET_SDA;
+ TR_SET_SCL;
+ TR_DELAY;
+ TR_CLR_SDA;
+ TR_CLR_SCL;
+ TR_DELAY;
+
+ for (; n; n--) {
+ val = va_arg(args, unsigned);
+ for (mask = 0x80; mask; mask >>= 1) {
+ if (val & mask)
+ TR_SET_SDA;
+ else
+ TR_CLR_SDA;
+ TR_SET_SCL;
+ TR_DELAY;
+ TR_CLR_SCL;
+ TR_DELAY;
+ }
+ /* acknowledge bit */
+ TR_SET_SDA;
+ TR_SET_SCL;
+ TR_DELAY;
+ TR_CLR_SCL;
+ TR_DELAY;
+ }
+
+ /* stop condition */
+ TR_CLR_SDA;
+ TR_DELAY;
+ TR_SET_SCL;
+ TR_DELAY;
+ TR_SET_SDA;
+ TR_DELAY;
+
+ va_end(args);
+}
+
+static int trust_s_mute_volume(struct radio_isa_card *isa, bool mute, int vol)
+{
+ struct trust *tr = container_of(isa, struct trust, isa);
+
+ tr->ioval = (tr->ioval & 0xf7) | (mute << 3);
+ outb(tr->ioval, isa->io);
+ write_i2c(tr, 2, TDA7318_ADDR, vol ^ 0x1f);
+ return 0;
+}
+
+static int trust_s_stereo(struct radio_isa_card *isa, bool stereo)
+{
+ struct trust *tr = container_of(isa, struct trust, isa);
+
+ tr->ioval = (tr->ioval & 0xfb) | (!stereo << 2);
+ outb(tr->ioval, isa->io);
+ return 0;
+}
+
+static u32 trust_g_signal(struct radio_isa_card *isa)
+{
+ int i, v;
+
+ for (i = 0, v = 0; i < 100; i++)
+ v |= inb(isa->io);
+ return (v & 1) ? 0 : 0xffff;
+}
+
+static int trust_s_frequency(struct radio_isa_card *isa, u32 freq)
+{
+ struct trust *tr = container_of(isa, struct trust, isa);
+
+ freq /= 160; /* Convert to 10 kHz units */
+ freq += 1070; /* Add 10.7 MHz IF */
+ write_i2c(tr, 5, TSA6060T_ADDR, (freq << 1) | 1,
+ freq >> 7, 0x60 | ((freq >> 15) & 1), 0);
+ return 0;
+}
+
+static int basstreble2chip[15] = {
+ 0, 1, 2, 3, 4, 5, 6, 7, 14, 13, 12, 11, 10, 9, 8
+};
+
+static int trust_s_ctrl(struct v4l2_ctrl *ctrl)
+{
+ struct radio_isa_card *isa =
+ container_of(ctrl->handler, struct radio_isa_card, hdl);
+ struct trust *tr = container_of(isa, struct trust, isa);
+
+ switch (ctrl->id) {
+ case V4L2_CID_AUDIO_BASS:
+ write_i2c(tr, 2, TDA7318_ADDR, 0x60 | basstreble2chip[ctrl->val]);
+ return 0;
+ case V4L2_CID_AUDIO_TREBLE:
+ write_i2c(tr, 2, TDA7318_ADDR, 0x70 | basstreble2chip[ctrl->val]);
+ return 0;
+ }
+ return -EINVAL;
+}
+
+static const struct v4l2_ctrl_ops trust_ctrl_ops = {
+ .s_ctrl = trust_s_ctrl,
+};
+
+static int trust_initialize(struct radio_isa_card *isa)
+{
+ struct trust *tr = container_of(isa, struct trust, isa);
+
+ tr->ioval = 0xf;
+ write_i2c(tr, 2, TDA7318_ADDR, 0x80); /* speaker att. LF = 0 dB */
+ write_i2c(tr, 2, TDA7318_ADDR, 0xa0); /* speaker att. RF = 0 dB */
+ write_i2c(tr, 2, TDA7318_ADDR, 0xc0); /* speaker att. LR = 0 dB */
+ write_i2c(tr, 2, TDA7318_ADDR, 0xe0); /* speaker att. RR = 0 dB */
+ write_i2c(tr, 2, TDA7318_ADDR, 0x40); /* stereo 1 input, gain = 18.75 dB */
+
+ v4l2_ctrl_new_std(&isa->hdl, &trust_ctrl_ops,
+ V4L2_CID_AUDIO_BASS, 0, 15, 1, 8);
+ v4l2_ctrl_new_std(&isa->hdl, &trust_ctrl_ops,
+ V4L2_CID_AUDIO_TREBLE, 0, 15, 1, 8);
+ return isa->hdl.error;
+}
+
+static const struct radio_isa_ops trust_ops = {
+ .init = trust_initialize,
+ .alloc = trust_alloc,
+ .s_mute_volume = trust_s_mute_volume,
+ .s_frequency = trust_s_frequency,
+ .s_stereo = trust_s_stereo,
+ .g_signal = trust_g_signal,
+};
+
+static const int trust_ioports[] = { 0x350, 0x358 };
+
+static struct radio_isa_driver trust_driver = {
+ .driver = {
+ .match = radio_isa_match,
+ .probe = radio_isa_probe,
+ .remove = radio_isa_remove,
+ .driver = {
+ .name = "radio-trust",
+ },
+ },
+ .io_params = io,
+ .radio_nr_params = radio_nr,
+ .io_ports = trust_ioports,
+ .num_of_io_ports = ARRAY_SIZE(trust_ioports),
+ .region_size = 2,
+ .card = "Trust FM Radio",
+ .ops = &trust_ops,
+ .has_stereo = true,
+ .max_volume = 31,
+};
+
+static int __init trust_init(void)
+{
+ return isa_register_driver(&trust_driver.driver, TRUST_MAX);
+}
+
+static void __exit trust_exit(void)
+{
+ isa_unregister_driver(&trust_driver.driver);
+}
+
+module_init(trust_init);
+module_exit(trust_exit);
diff --git a/drivers/media/radio/radio-typhoon.c b/drivers/media/radio/radio-typhoon.c
new file mode 100644
index 000000000..3454c9a6f
--- /dev/null
+++ b/drivers/media/radio/radio-typhoon.c
@@ -0,0 +1,181 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Typhoon Radio Card driver for radio support
+ * (c) 1999 Dr. Henrik Seidel <Henrik.Seidel@gmx.de>
+ *
+ * Notes on the hardware
+ *
+ * This card has two output sockets, one for speakers and one for line.
+ * The speaker output has volume control, but only in four discrete
+ * steps. The line output has neither volume control nor mute.
+ *
+ * The card has auto-stereo according to its manual, although it all
+ * sounds mono to me (even with the Win/DOS drivers). Maybe it's my
+ * antenna - I really don't know for sure.
+ *
+ * Frequency control is done digitally.
+ *
+ * Volume control is done digitally, but there are only four different
+ * possible values. So you should better always turn the volume up and
+ * use line control. I got the best results by connecting line output
+ * to the sound card microphone input. For such a configuration the
+ * volume control has no effect, since volume control only influences
+ * the speaker output.
+ *
+ * There is no explicit mute/unmute. So I set the radio frequency to a
+ * value where I do expect just noise and turn the speaker volume down.
+ * The frequency change is necessary since the card never seems to be
+ * completely silent.
+ *
+ * Converted to V4L2 API by Mauro Carvalho Chehab <mchehab@kernel.org>
+ */
+
+#include <linux/module.h> /* Modules */
+#include <linux/init.h> /* Initdata */
+#include <linux/ioport.h> /* request_region */
+#include <linux/videodev2.h> /* kernel radio structs */
+#include <linux/io.h> /* outb, outb_p */
+#include <linux/slab.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-ioctl.h>
+#include "radio-isa.h"
+
+#define DRIVER_VERSION "0.1.2"
+
+MODULE_AUTHOR("Dr. Henrik Seidel");
+MODULE_DESCRIPTION("A driver for the Typhoon radio card (a.k.a. EcoRadio).");
+MODULE_LICENSE("GPL");
+MODULE_VERSION("0.1.99");
+
+#ifndef CONFIG_RADIO_TYPHOON_PORT
+#define CONFIG_RADIO_TYPHOON_PORT -1
+#endif
+
+#ifndef CONFIG_RADIO_TYPHOON_MUTEFREQ
+#define CONFIG_RADIO_TYPHOON_MUTEFREQ 87000
+#endif
+
+#define TYPHOON_MAX 2
+
+static int io[TYPHOON_MAX] = { [0] = CONFIG_RADIO_TYPHOON_PORT,
+ [1 ... (TYPHOON_MAX - 1)] = -1 };
+static int radio_nr[TYPHOON_MAX] = { [0 ... (TYPHOON_MAX - 1)] = -1 };
+static unsigned long mutefreq = CONFIG_RADIO_TYPHOON_MUTEFREQ;
+
+module_param_array(io, int, NULL, 0444);
+MODULE_PARM_DESC(io, "I/O addresses of the Typhoon card (0x316 or 0x336)");
+module_param_array(radio_nr, int, NULL, 0444);
+MODULE_PARM_DESC(radio_nr, "Radio device numbers");
+module_param(mutefreq, ulong, 0);
+MODULE_PARM_DESC(mutefreq, "Frequency used when muting the card (in kHz)");
+
+struct typhoon {
+ struct radio_isa_card isa;
+ int muted;
+};
+
+static struct radio_isa_card *typhoon_alloc(void)
+{
+ struct typhoon *ty = kzalloc_obj(*ty);
+
+ return ty ? &ty->isa : NULL;
+}
+
+static int typhoon_s_frequency(struct radio_isa_card *isa, u32 freq)
+{
+ unsigned long outval;
+ unsigned long x;
+
+ /*
+ * The frequency transfer curve is not linear. The best fit I could
+ * get is
+ *
+ * outval = -155 + exp((f + 15.55) * 0.057))
+ *
+ * where frequency f is in MHz. Since we don't have exp in the kernel,
+ * I approximate this function by a third order polynomial.
+ *
+ */
+
+ x = freq / 160;
+ outval = (x * x + 2500) / 5000;
+ outval = (outval * x + 5000) / 10000;
+ outval -= (10 * x * x + 10433) / 20866;
+ outval += 4 * x - 11505;
+
+ outb_p((outval >> 8) & 0x01, isa->io + 4);
+ outb_p(outval >> 9, isa->io + 6);
+ outb_p(outval & 0xff, isa->io + 8);
+ return 0;
+}
+
+static int typhoon_s_mute_volume(struct radio_isa_card *isa, bool mute, int vol)
+{
+ struct typhoon *ty = container_of(isa, struct typhoon, isa);
+
+ if (mute)
+ vol = 0;
+ vol >>= 14; /* Map 16 bit to 2 bit */
+ vol &= 3;
+ outb_p(vol / 2, isa->io); /* Set the volume, high bit. */
+ outb_p(vol % 2, isa->io + 2); /* Set the volume, low bit. */
+
+ if (vol == 0 && !ty->muted) {
+ ty->muted = true;
+ return typhoon_s_frequency(isa, mutefreq << 4);
+ }
+ if (vol && ty->muted) {
+ ty->muted = false;
+ return typhoon_s_frequency(isa, isa->freq);
+ }
+ return 0;
+}
+
+static const struct radio_isa_ops typhoon_ops = {
+ .alloc = typhoon_alloc,
+ .s_mute_volume = typhoon_s_mute_volume,
+ .s_frequency = typhoon_s_frequency,
+};
+
+static const int typhoon_ioports[] = { 0x316, 0x336 };
+
+static struct radio_isa_driver typhoon_driver = {
+ .driver = {
+ .match = radio_isa_match,
+ .probe = radio_isa_probe,
+ .remove = radio_isa_remove,
+ .driver = {
+ .name = "radio-typhoon",
+ },
+ },
+ .io_params = io,
+ .radio_nr_params = radio_nr,
+ .io_ports = typhoon_ioports,
+ .num_of_io_ports = ARRAY_SIZE(typhoon_ioports),
+ .region_size = 8,
+ .card = "Typhoon Radio",
+ .ops = &typhoon_ops,
+ .has_stereo = true,
+ .max_volume = 3,
+};
+
+static int __init typhoon_init(void)
+{
+ if (mutefreq < 87000 || mutefreq > 108000) {
+ printk(KERN_ERR "%s: You must set a frequency (in kHz) used when muting the card,\n",
+ typhoon_driver.driver.driver.name);
+ printk(KERN_ERR "%s: e.g. with \"mutefreq=87500\" (87000 <= mutefreq <= 108000)\n",
+ typhoon_driver.driver.driver.name);
+ return -ENODEV;
+ }
+ return isa_register_driver(&typhoon_driver.driver, TYPHOON_MAX);
+}
+
+static void __exit typhoon_exit(void)
+{
+ isa_unregister_driver(&typhoon_driver.driver);
+}
+
+
+module_init(typhoon_init);
+module_exit(typhoon_exit);
+
diff --git a/drivers/media/radio/radio-zoltrix.c b/drivers/media/radio/radio-zoltrix.c
new file mode 100644
index 000000000..b92b24973
--- /dev/null
+++ b/drivers/media/radio/radio-zoltrix.c
@@ -0,0 +1,249 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Zoltrix Radio Plus driver
+ * Copyright 1998 C. van Schaik <carl@leg.uct.ac.za>
+ *
+ * BUGS
+ * Due to the inconsistency in reading from the signal flags
+ * it is difficult to get an accurate tuned signal.
+ *
+ * It seems that the card is not linear to 0 volume. It cuts off
+ * at a low volume, and it is not possible (at least I have not found)
+ * to get fine volume control over the low volume range.
+ *
+ * Some code derived from code by Romolo Manfredini
+ * romolo@bicnet.it
+ *
+ * 1999-05-06 - (C. van Schaik)
+ * - Make signal strength and stereo scans
+ * kinder to cpu while in delay
+ * 1999-01-05 - (C. van Schaik)
+ * - Changed tuning to 1/160Mhz accuracy
+ * - Added stereo support
+ * (card defaults to stereo)
+ * (can explicitly force mono on the card)
+ * (can detect if station is in stereo)
+ * - Added unmute function
+ * - Reworked ioctl functions
+ * 2002-07-15 - Fix Stereo typo
+ *
+ * 2006-07-24 - Converted to V4L2 API
+ * by Mauro Carvalho Chehab <mchehab@kernel.org>
+ *
+ * Converted to the radio-isa framework by Hans Verkuil <hverkuil@kernel.org>
+ *
+ * Note that this is the driver for the Zoltrix Radio Plus.
+ * This driver does not work for the Zoltrix Radio Plus 108 or the
+ * Zoltrix Radio Plus for Windows.
+ *
+ * Fully tested with the Keene USB FM Transmitter and the v4l2-compliance tool.
+ */
+
+#include <linux/module.h> /* Modules */
+#include <linux/init.h> /* Initdata */
+#include <linux/ioport.h> /* request_region */
+#include <linux/delay.h> /* udelay, msleep */
+#include <linux/videodev2.h> /* kernel radio structs */
+#include <linux/mutex.h>
+#include <linux/io.h> /* outb, outb_p */
+#include <linux/slab.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-ioctl.h>
+#include "radio-isa.h"
+
+MODULE_AUTHOR("C. van Schaik");
+MODULE_DESCRIPTION("A driver for the Zoltrix Radio Plus.");
+MODULE_LICENSE("GPL");
+MODULE_VERSION("0.1.99");
+
+#ifndef CONFIG_RADIO_ZOLTRIX_PORT
+#define CONFIG_RADIO_ZOLTRIX_PORT -1
+#endif
+
+#define ZOLTRIX_MAX 2
+
+static int io[ZOLTRIX_MAX] = { [0] = CONFIG_RADIO_ZOLTRIX_PORT,
+ [1 ... (ZOLTRIX_MAX - 1)] = -1 };
+static int radio_nr[ZOLTRIX_MAX] = { [0 ... (ZOLTRIX_MAX - 1)] = -1 };
+
+module_param_array(io, int, NULL, 0444);
+MODULE_PARM_DESC(io, "I/O addresses of the Zoltrix Radio Plus card (0x20c or 0x30c)");
+module_param_array(radio_nr, int, NULL, 0444);
+MODULE_PARM_DESC(radio_nr, "Radio device numbers");
+
+struct zoltrix {
+ struct radio_isa_card isa;
+ int curvol;
+ bool muted;
+};
+
+static struct radio_isa_card *zoltrix_alloc(void)
+{
+ struct zoltrix *zol = kzalloc_obj(*zol);
+
+ return zol ? &zol->isa : NULL;
+}
+
+static int zoltrix_s_mute_volume(struct radio_isa_card *isa, bool mute, int vol)
+{
+ struct zoltrix *zol = container_of(isa, struct zoltrix, isa);
+
+ zol->curvol = vol;
+ zol->muted = mute;
+ if (mute || vol == 0) {
+ outb(0, isa->io);
+ outb(0, isa->io);
+ inb(isa->io + 3); /* Zoltrix needs to be read to confirm */
+ return 0;
+ }
+
+ outb(vol - 1, isa->io);
+ msleep(10);
+ inb(isa->io + 2);
+ return 0;
+}
+
+/* tunes the radio to the desired frequency */
+static int zoltrix_s_frequency(struct radio_isa_card *isa, u32 freq)
+{
+ struct zoltrix *zol = container_of(isa, struct zoltrix, isa);
+ struct v4l2_device *v4l2_dev = &isa->v4l2_dev;
+ unsigned long long bitmask, f, m;
+ bool stereo = isa->stereo;
+ int i;
+
+ if (freq == 0) {
+ v4l2_warn(v4l2_dev, "cannot set a frequency of 0.\n");
+ return -EINVAL;
+ }
+
+ m = (freq / 160 - 8800) * 2;
+ f = (unsigned long long)m + 0x4d1c;
+
+ bitmask = 0xc480402c10080000ull;
+ i = 45;
+
+ outb(0, isa->io);
+ outb(0, isa->io);
+ inb(isa->io + 3); /* Zoltrix needs to be read to confirm */
+
+ outb(0x40, isa->io);
+ outb(0xc0, isa->io);
+
+ bitmask = (bitmask ^ ((f & 0xff) << 47) ^ ((f & 0xff00) << 30) ^ (stereo << 31));
+ while (i--) {
+ if ((bitmask & 0x8000000000000000ull) != 0) {
+ outb(0x80, isa->io);
+ udelay(50);
+ outb(0x00, isa->io);
+ udelay(50);
+ outb(0x80, isa->io);
+ udelay(50);
+ } else {
+ outb(0xc0, isa->io);
+ udelay(50);
+ outb(0x40, isa->io);
+ udelay(50);
+ outb(0xc0, isa->io);
+ udelay(50);
+ }
+ bitmask *= 2;
+ }
+ /* termination sequence */
+ outb(0x80, isa->io);
+ outb(0xc0, isa->io);
+ outb(0x40, isa->io);
+ udelay(1000);
+ inb(isa->io + 2);
+ udelay(1000);
+
+ return zoltrix_s_mute_volume(isa, zol->muted, zol->curvol);
+}
+
+/* Get signal strength */
+static u32 zoltrix_g_rxsubchans(struct radio_isa_card *isa)
+{
+ struct zoltrix *zol = container_of(isa, struct zoltrix, isa);
+ int a, b;
+
+ outb(0x00, isa->io); /* This stuff I found to do nothing */
+ outb(zol->curvol, isa->io);
+ msleep(20);
+
+ a = inb(isa->io);
+ msleep(10);
+ b = inb(isa->io);
+
+ return (a == b && a == 0xcf) ?
+ V4L2_TUNER_SUB_STEREO : V4L2_TUNER_SUB_MONO;
+}
+
+static u32 zoltrix_g_signal(struct radio_isa_card *isa)
+{
+ struct zoltrix *zol = container_of(isa, struct zoltrix, isa);
+ int a, b;
+
+ outb(0x00, isa->io); /* This stuff I found to do nothing */
+ outb(zol->curvol, isa->io);
+ msleep(20);
+
+ a = inb(isa->io);
+ msleep(10);
+ b = inb(isa->io);
+
+ if (a != b)
+ return 0;
+
+ /* I found this out by playing with a binary scanner on the card io */
+ return (a == 0xcf || a == 0xdf || a == 0xef) ? 0xffff : 0;
+}
+
+static int zoltrix_s_stereo(struct radio_isa_card *isa, bool stereo)
+{
+ return zoltrix_s_frequency(isa, isa->freq);
+}
+
+static const struct radio_isa_ops zoltrix_ops = {
+ .alloc = zoltrix_alloc,
+ .s_mute_volume = zoltrix_s_mute_volume,
+ .s_frequency = zoltrix_s_frequency,
+ .s_stereo = zoltrix_s_stereo,
+ .g_rxsubchans = zoltrix_g_rxsubchans,
+ .g_signal = zoltrix_g_signal,
+};
+
+static const int zoltrix_ioports[] = { 0x20c, 0x30c };
+
+static struct radio_isa_driver zoltrix_driver = {
+ .driver = {
+ .match = radio_isa_match,
+ .probe = radio_isa_probe,
+ .remove = radio_isa_remove,
+ .driver = {
+ .name = "radio-zoltrix",
+ },
+ },
+ .io_params = io,
+ .radio_nr_params = radio_nr,
+ .io_ports = zoltrix_ioports,
+ .num_of_io_ports = ARRAY_SIZE(zoltrix_ioports),
+ .region_size = 2,
+ .card = "Zoltrix Radio Plus",
+ .ops = &zoltrix_ops,
+ .has_stereo = true,
+ .max_volume = 15,
+};
+
+static int __init zoltrix_init(void)
+{
+ return isa_register_driver(&zoltrix_driver.driver, ZOLTRIX_MAX);
+}
+
+static void __exit zoltrix_exit(void)
+{
+ isa_unregister_driver(&zoltrix_driver.driver);
+}
+
+module_init(zoltrix_init);
+module_exit(zoltrix_exit);
+
diff --git a/drivers/media/radio/saa7706h.c b/drivers/media/radio/saa7706h.c
new file mode 100644
index 000000000..bd8bd295a
--- /dev/null
+++ b/drivers/media/radio/saa7706h.c
@@ -0,0 +1,417 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * saa7706.c Philips SAA7706H Car Radio DSP driver
+ * Copyright (c) 2009 Intel Corporation
+ */
+
+#include <linux/module.h>
+#include <linux/init.h>
+#include <linux/delay.h>
+#include <linux/errno.h>
+#include <linux/kernel.h>
+#include <linux/interrupt.h>
+#include <linux/i2c.h>
+#include <linux/slab.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-ctrls.h>
+
+#define DRIVER_NAME "saa7706h"
+
+/* the I2C memory map looks like this
+
+ $1C00 - $FFFF Not Used
+ $2200 - $3FFF Reserved YRAM (DSP2) space
+ $2000 - $21FF YRAM (DSP2)
+ $1FF0 - $1FFF Hardware Registers
+ $1280 - $1FEF Reserved XRAM (DSP2) space
+ $1000 - $127F XRAM (DSP2)
+ $0FFF DSP CONTROL
+ $0A00 - $0FFE Reserved
+ $0980 - $09FF Reserved YRAM (DSP1) space
+ $0800 - $097F YRAM (DSP1)
+ $0200 - $07FF Not Used
+ $0180 - $01FF Reserved XRAM (DSP1) space
+ $0000 - $017F XRAM (DSP1)
+*/
+
+#define SAA7706H_REG_CTRL 0x0fff
+#define SAA7706H_CTRL_BYP_PLL 0x0001
+#define SAA7706H_CTRL_PLL_DIV_MASK 0x003e
+#define SAA7706H_CTRL_PLL3_62975MHZ 0x003e
+#define SAA7706H_CTRL_DSP_TURBO 0x0040
+#define SAA7706H_CTRL_PC_RESET_DSP1 0x0080
+#define SAA7706H_CTRL_PC_RESET_DSP2 0x0100
+#define SAA7706H_CTRL_DSP1_ROM_EN_MASK 0x0600
+#define SAA7706H_CTRL_DSP1_FUNC_PROM 0x0000
+#define SAA7706H_CTRL_DSP2_ROM_EN_MASK 0x1800
+#define SAA7706H_CTRL_DSP2_FUNC_PROM 0x0000
+#define SAA7706H_CTRL_DIG_SIL_INTERPOL 0x8000
+
+#define SAA7706H_REG_EVALUATION 0x1ff0
+#define SAA7706H_EVAL_DISABLE_CHARGE_PUMP 0x000001
+#define SAA7706H_EVAL_DCS_CLOCK 0x000002
+#define SAA7706H_EVAL_GNDRC1_ENABLE 0x000004
+#define SAA7706H_EVAL_GNDRC2_ENABLE 0x000008
+
+#define SAA7706H_REG_CL_GEN1 0x1ff3
+#define SAA7706H_CL_GEN1_MIN_LOOPGAIN_MASK 0x00000f
+#define SAA7706H_CL_GEN1_LOOPGAIN_MASK 0x0000f0
+#define SAA7706H_CL_GEN1_COARSE_RATION 0xffff00
+
+#define SAA7706H_REG_CL_GEN2 0x1ff4
+#define SAA7706H_CL_GEN2_WSEDGE_FALLING 0x000001
+#define SAA7706H_CL_GEN2_STOP_VCO 0x000002
+#define SAA7706H_CL_GEN2_FRERUN 0x000004
+#define SAA7706H_CL_GEN2_ADAPTIVE 0x000008
+#define SAA7706H_CL_GEN2_FINE_RATIO_MASK 0x0ffff0
+
+#define SAA7706H_REG_CL_GEN4 0x1ff6
+#define SAA7706H_CL_GEN4_BYPASS_PLL1 0x001000
+#define SAA7706H_CL_GEN4_PLL1_DIV_MASK 0x03e000
+#define SAA7706H_CL_GEN4_DSP1_TURBO 0x040000
+
+#define SAA7706H_REG_SEL 0x1ff7
+#define SAA7706H_SEL_DSP2_SRCA_MASK 0x000007
+#define SAA7706H_SEL_DSP2_FMTA_MASK 0x000031
+#define SAA7706H_SEL_DSP2_SRCB_MASK 0x0001c0
+#define SAA7706H_SEL_DSP2_FMTB_MASK 0x000e00
+#define SAA7706H_SEL_DSP1_SRC_MASK 0x003000
+#define SAA7706H_SEL_DSP1_FMT_MASK 0x01c003
+#define SAA7706H_SEL_SPDIF2 0x020000
+#define SAA7706H_SEL_HOST_IO_FMT_MASK 0x1c0000
+#define SAA7706H_SEL_EN_HOST_IO 0x200000
+
+#define SAA7706H_REG_IAC 0x1ff8
+#define SAA7706H_REG_CLK_SET 0x1ff9
+#define SAA7706H_REG_CLK_COEFF 0x1ffa
+#define SAA7706H_REG_INPUT_SENS 0x1ffb
+#define SAA7706H_INPUT_SENS_RDS_VOL_MASK 0x0003f
+#define SAA7706H_INPUT_SENS_FM_VOL_MASK 0x00fc0
+#define SAA7706H_INPUT_SENS_FM_MPX 0x01000
+#define SAA7706H_INPUT_SENS_OFF_FILTER_A_EN 0x02000
+#define SAA7706H_INPUT_SENS_OFF_FILTER_B_EN 0x04000
+#define SAA7706H_REG_PHONE_NAV_AUDIO 0x1ffc
+#define SAA7706H_REG_IO_CONF_DSP2 0x1ffd
+#define SAA7706H_REG_STATUS_DSP2 0x1ffe
+#define SAA7706H_REG_PC_DSP2 0x1fff
+
+#define SAA7706H_DSP1_MOD0 0x0800
+#define SAA7706H_DSP1_ROM_VER 0x097f
+#define SAA7706H_DSP2_MPTR0 0x1000
+
+#define SAA7706H_DSP1_MODPNTR 0x0000
+
+#define SAA7706H_DSP2_XMEM_CONTLLCW 0x113e
+#define SAA7706H_DSP2_XMEM_BUSAMP 0x114a
+#define SAA7706H_DSP2_XMEM_FDACPNTR 0x11f9
+#define SAA7706H_DSP2_XMEM_IIS1PNTR 0x11fb
+
+#define SAA7706H_DSP2_YMEM_PVGA 0x212a
+#define SAA7706H_DSP2_YMEM_PVAT1 0x212b
+#define SAA7706H_DSP2_YMEM_PVAT 0x212c
+#define SAA7706H_DSP2_YMEM_ROM_VER 0x21ff
+
+#define SUPPORTED_DSP1_ROM_VER 0x667
+
+struct saa7706h_state {
+ struct v4l2_subdev sd;
+ struct v4l2_ctrl_handler hdl;
+ unsigned muted;
+};
+
+static inline struct saa7706h_state *to_state(struct v4l2_subdev *sd)
+{
+ return container_of(sd, struct saa7706h_state, sd);
+}
+
+static int saa7706h_i2c_send(struct i2c_client *client, const u8 *data, int len)
+{
+ int err = i2c_master_send(client, data, len);
+ if (err == len)
+ return 0;
+ return err > 0 ? -EIO : err;
+}
+
+static int saa7706h_i2c_transfer(struct i2c_client *client,
+ struct i2c_msg *msgs, int num)
+{
+ int err = i2c_transfer(client->adapter, msgs, num);
+ if (err == num)
+ return 0;
+ return err > 0 ? -EIO : err;
+}
+
+static int saa7706h_set_reg24(struct v4l2_subdev *sd, u16 reg, u32 val)
+{
+ struct i2c_client *client = v4l2_get_subdevdata(sd);
+ u8 buf[5];
+ int pos = 0;
+
+ buf[pos++] = reg >> 8;
+ buf[pos++] = reg;
+ buf[pos++] = val >> 16;
+ buf[pos++] = val >> 8;
+ buf[pos++] = val;
+
+ return saa7706h_i2c_send(client, buf, pos);
+}
+
+static int saa7706h_set_reg24_err(struct v4l2_subdev *sd, u16 reg, u32 val,
+ int *err)
+{
+ return *err ? *err : saa7706h_set_reg24(sd, reg, val);
+}
+
+static int saa7706h_set_reg16(struct v4l2_subdev *sd, u16 reg, u16 val)
+{
+ struct i2c_client *client = v4l2_get_subdevdata(sd);
+ u8 buf[4];
+ int pos = 0;
+
+ buf[pos++] = reg >> 8;
+ buf[pos++] = reg;
+ buf[pos++] = val >> 8;
+ buf[pos++] = val;
+
+ return saa7706h_i2c_send(client, buf, pos);
+}
+
+static int saa7706h_set_reg16_err(struct v4l2_subdev *sd, u16 reg, u16 val,
+ int *err)
+{
+ return *err ? *err : saa7706h_set_reg16(sd, reg, val);
+}
+
+static int saa7706h_get_reg16(struct v4l2_subdev *sd, u16 reg)
+{
+ struct i2c_client *client = v4l2_get_subdevdata(sd);
+ u8 buf[2];
+ int err;
+ u8 regaddr[] = {reg >> 8, reg};
+ struct i2c_msg msg[] = {
+ {
+ .addr = client->addr,
+ .len = sizeof(regaddr),
+ .buf = regaddr
+ },
+ {
+ .addr = client->addr,
+ .flags = I2C_M_RD,
+ .len = sizeof(buf),
+ .buf = buf
+ }
+ };
+
+ err = saa7706h_i2c_transfer(client, msg, ARRAY_SIZE(msg));
+ if (err)
+ return err;
+
+ return buf[0] << 8 | buf[1];
+}
+
+static int saa7706h_unmute(struct v4l2_subdev *sd)
+{
+ struct saa7706h_state *state = to_state(sd);
+ int err = 0;
+
+ err = saa7706h_set_reg16_err(sd, SAA7706H_REG_CTRL,
+ SAA7706H_CTRL_PLL3_62975MHZ | SAA7706H_CTRL_PC_RESET_DSP1 |
+ SAA7706H_CTRL_PC_RESET_DSP2, &err);
+
+ /* newer versions of the chip requires a small sleep after reset */
+ msleep(1);
+
+ err = saa7706h_set_reg16_err(sd, SAA7706H_REG_CTRL,
+ SAA7706H_CTRL_PLL3_62975MHZ, &err);
+
+ err = saa7706h_set_reg24_err(sd, SAA7706H_REG_EVALUATION, 0, &err);
+
+ err = saa7706h_set_reg24_err(sd, SAA7706H_REG_CL_GEN1, 0x040022, &err);
+
+ err = saa7706h_set_reg24_err(sd, SAA7706H_REG_CL_GEN2,
+ SAA7706H_CL_GEN2_WSEDGE_FALLING, &err);
+
+ err = saa7706h_set_reg24_err(sd, SAA7706H_REG_CL_GEN4, 0x024080, &err);
+
+ err = saa7706h_set_reg24_err(sd, SAA7706H_REG_SEL, 0x200080, &err);
+
+ err = saa7706h_set_reg24_err(sd, SAA7706H_REG_IAC, 0xf4caed, &err);
+
+ err = saa7706h_set_reg24_err(sd, SAA7706H_REG_CLK_SET, 0x124334, &err);
+
+ err = saa7706h_set_reg24_err(sd, SAA7706H_REG_CLK_COEFF, 0x004a1a,
+ &err);
+
+ err = saa7706h_set_reg24_err(sd, SAA7706H_REG_INPUT_SENS, 0x0071c7,
+ &err);
+
+ err = saa7706h_set_reg24_err(sd, SAA7706H_REG_PHONE_NAV_AUDIO,
+ 0x0e22ff, &err);
+
+ err = saa7706h_set_reg24_err(sd, SAA7706H_REG_IO_CONF_DSP2, 0x001ff8,
+ &err);
+
+ err = saa7706h_set_reg24_err(sd, SAA7706H_REG_STATUS_DSP2, 0x080003,
+ &err);
+
+ err = saa7706h_set_reg24_err(sd, SAA7706H_REG_PC_DSP2, 0x000004, &err);
+
+ err = saa7706h_set_reg16_err(sd, SAA7706H_DSP1_MOD0, 0x0c6c, &err);
+
+ err = saa7706h_set_reg24_err(sd, SAA7706H_DSP2_MPTR0, 0x000b4b, &err);
+
+ err = saa7706h_set_reg24_err(sd, SAA7706H_DSP1_MODPNTR, 0x000600, &err);
+
+ err = saa7706h_set_reg24_err(sd, SAA7706H_DSP1_MODPNTR, 0x0000c0, &err);
+
+ err = saa7706h_set_reg24_err(sd, SAA7706H_DSP2_XMEM_CONTLLCW, 0x000819,
+ &err);
+
+ err = saa7706h_set_reg24_err(sd, SAA7706H_DSP2_XMEM_CONTLLCW, 0x00085a,
+ &err);
+
+ err = saa7706h_set_reg24_err(sd, SAA7706H_DSP2_XMEM_BUSAMP, 0x7fffff,
+ &err);
+
+ err = saa7706h_set_reg24_err(sd, SAA7706H_DSP2_XMEM_FDACPNTR, 0x2000cb,
+ &err);
+
+ err = saa7706h_set_reg24_err(sd, SAA7706H_DSP2_XMEM_IIS1PNTR, 0x2000cb,
+ &err);
+
+ err = saa7706h_set_reg16_err(sd, SAA7706H_DSP2_YMEM_PVGA, 0x0f80, &err);
+
+ err = saa7706h_set_reg16_err(sd, SAA7706H_DSP2_YMEM_PVAT1, 0x0800,
+ &err);
+
+ err = saa7706h_set_reg16_err(sd, SAA7706H_DSP2_YMEM_PVAT, 0x0800, &err);
+
+ err = saa7706h_set_reg24_err(sd, SAA7706H_DSP2_XMEM_CONTLLCW, 0x000905,
+ &err);
+ if (!err)
+ state->muted = 0;
+ return err;
+}
+
+static int saa7706h_mute(struct v4l2_subdev *sd)
+{
+ struct saa7706h_state *state = to_state(sd);
+ int err;
+
+ err = saa7706h_set_reg16(sd, SAA7706H_REG_CTRL,
+ SAA7706H_CTRL_PLL3_62975MHZ | SAA7706H_CTRL_PC_RESET_DSP1 |
+ SAA7706H_CTRL_PC_RESET_DSP2);
+ if (!err)
+ state->muted = 1;
+ return err;
+}
+
+static int saa7706h_s_ctrl(struct v4l2_ctrl *ctrl)
+{
+ struct saa7706h_state *state =
+ container_of(ctrl->handler, struct saa7706h_state, hdl);
+
+ switch (ctrl->id) {
+ case V4L2_CID_AUDIO_MUTE:
+ if (ctrl->val)
+ return saa7706h_mute(&state->sd);
+ return saa7706h_unmute(&state->sd);
+ }
+ return -EINVAL;
+}
+
+static const struct v4l2_ctrl_ops saa7706h_ctrl_ops = {
+ .s_ctrl = saa7706h_s_ctrl,
+};
+
+static const struct v4l2_subdev_ops empty_ops = {};
+
+/*
+ * Generic i2c probe
+ * concerning the addresses: i2c wants 7 bit (without the r/w bit), so '>>1'
+ */
+
+static int saa7706h_probe(struct i2c_client *client)
+{
+ struct saa7706h_state *state;
+ struct v4l2_subdev *sd;
+ int err;
+
+ /* Check if the adapter supports the needed features */
+ if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_BYTE_DATA))
+ return -EIO;
+
+ v4l_info(client, "chip found @ 0x%02x (%s)\n",
+ client->addr << 1, client->adapter->name);
+
+ state = kzalloc_obj(struct saa7706h_state);
+ if (state == NULL)
+ return -ENOMEM;
+ sd = &state->sd;
+ v4l2_i2c_subdev_init(sd, client, &empty_ops);
+
+ v4l2_ctrl_handler_init(&state->hdl, 4);
+ v4l2_ctrl_new_std(&state->hdl, &saa7706h_ctrl_ops,
+ V4L2_CID_AUDIO_MUTE, 0, 1, 1, 1);
+ sd->ctrl_handler = &state->hdl;
+ err = state->hdl.error;
+ if (err)
+ goto err;
+
+ /* check the rom versions */
+ err = saa7706h_get_reg16(sd, SAA7706H_DSP1_ROM_VER);
+ if (err < 0)
+ goto err;
+ if (err != SUPPORTED_DSP1_ROM_VER)
+ v4l2_warn(sd, "Unknown DSP1 ROM code version: 0x%x\n", err);
+ state->muted = 1;
+
+ /* startup in a muted state */
+ err = saa7706h_mute(sd);
+ if (err)
+ goto err;
+
+ return 0;
+
+err:
+ v4l2_device_unregister_subdev(sd);
+ v4l2_ctrl_handler_free(&state->hdl);
+ kfree(to_state(sd));
+
+ printk(KERN_ERR DRIVER_NAME ": Failed to probe: %d\n", err);
+
+ return err;
+}
+
+static void saa7706h_remove(struct i2c_client *client)
+{
+ struct v4l2_subdev *sd = i2c_get_clientdata(client);
+ struct saa7706h_state *state = to_state(sd);
+
+ saa7706h_mute(sd);
+ v4l2_device_unregister_subdev(sd);
+ v4l2_ctrl_handler_free(&state->hdl);
+ kfree(to_state(sd));
+}
+
+static const struct i2c_device_id saa7706h_id[] = {
+ { .name = DRIVER_NAME },
+ { }
+};
+
+MODULE_DEVICE_TABLE(i2c, saa7706h_id);
+
+static struct i2c_driver saa7706h_driver = {
+ .driver = {
+ .name = DRIVER_NAME,
+ },
+ .probe = saa7706h_probe,
+ .remove = saa7706h_remove,
+ .id_table = saa7706h_id,
+};
+
+module_i2c_driver(saa7706h_driver);
+
+MODULE_DESCRIPTION("SAA7706H Car Radio DSP driver");
+MODULE_AUTHOR("Mocean Laboratories");
+MODULE_LICENSE("GPL v2");
diff --git a/drivers/media/radio/si470x/Kconfig b/drivers/media/radio/si470x/Kconfig
new file mode 100644
index 000000000..9f7d35b04
--- /dev/null
+++ b/drivers/media/radio/si470x/Kconfig
@@ -0,0 +1,52 @@
+# SPDX-License-Identifier: GPL-2.0-only
+config RADIO_SI470X
+ tristate "Silicon Labs Si470x FM Radio Receiver support"
+ depends on VIDEO_DEV
+ help
+ This is a driver for devices with the Silicon Labs SI470x
+ chip (either via USB or I2C buses).
+
+ Say Y here if you want to connect this type of radio to your
+ computer's USB port or if it is used by some other driver
+ via I2C bus.
+
+ To compile this driver as a module, choose M here: the
+ module will be called radio-si470x-common.
+
+config USB_SI470X
+ tristate "Silicon Labs Si470x FM Radio Receiver support with USB"
+ depends on USB && RADIO_SI470X
+ help
+ This is a driver for USB devices with the Silicon Labs SI470x
+ chip. Currently these devices are known to work:
+ - 10c4:818a: Silicon Labs USB FM Radio Reference Design
+ - 06e1:a155: ADS/Tech FM Radio Receiver (formerly Instant FM Music)
+ - 1b80:d700: KWorld USB FM Radio SnapMusic Mobile 700 (FM700)
+ - 10c5:819a: Sanei Electric FM USB Radio (aka DealExtreme.com PCear)
+
+ Sound is provided by the ALSA USB Audio/MIDI driver. Therefore
+ if you don't want to use the device solely for RDS receiving,
+ it is recommended to also select SND_USB_AUDIO.
+
+ Please have a look at the documentation, especially on how
+ to redirect the audio stream from the radio to your sound device:
+ Documentation/admin-guide/media/si470x.rst
+
+ Say Y here if you want to connect this type of radio to your
+ computer's USB port.
+
+ To compile this driver as a module, choose M here: the
+ module will be called radio-usb-si470x.
+
+config I2C_SI470X
+ tristate "Silicon Labs Si470x FM Radio Receiver support with I2C"
+ depends on I2C && RADIO_SI470X
+ help
+ This is a driver for I2C devices with the Silicon Labs SI470x
+ chip.
+
+ Say Y here if you want to connect this type of radio to your
+ computer's I2C port.
+
+ To compile this driver as a module, choose M here: the
+ module will be called radio-i2c-si470x.
diff --git a/drivers/media/radio/si470x/Makefile b/drivers/media/radio/si470x/Makefile
new file mode 100644
index 000000000..e392126b0
--- /dev/null
+++ b/drivers/media/radio/si470x/Makefile
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Makefile for radios with Silicon Labs Si470x FM Radio Receivers
+#
+
+obj-$(CONFIG_RADIO_SI470X) += radio-si470x-common.o
+obj-$(CONFIG_USB_SI470X) += radio-si470x-usb.o
+obj-$(CONFIG_I2C_SI470X) += radio-si470x-i2c.o
diff --git a/drivers/media/radio/si470x/radio-si470x-common.c b/drivers/media/radio/si470x/radio-si470x-common.c
new file mode 100644
index 000000000..af0a70910
--- /dev/null
+++ b/drivers/media/radio/si470x/radio-si470x-common.c
@@ -0,0 +1,794 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * drivers/media/radio/si470x/radio-si470x-common.c
+ *
+ * Driver for radios with Silicon Labs Si470x FM Radio Receivers
+ *
+ * Copyright (c) 2009 Tobias Lorenz <tobias.lorenz@gmx.net>
+ * Copyright (c) 2012 Hans de Goede <hdegoede@redhat.com>
+ */
+
+
+/*
+ * History:
+ * 2008-01-12 Tobias Lorenz <tobias.lorenz@gmx.net>
+ * Version 1.0.0
+ * - First working version
+ * 2008-01-13 Tobias Lorenz <tobias.lorenz@gmx.net>
+ * Version 1.0.1
+ * - Improved error handling, every function now returns errno
+ * - Improved multi user access (start/mute/stop)
+ * - Channel doesn't get lost anymore after start/mute/stop
+ * - RDS support added (polling mode via interrupt EP 1)
+ * - marked default module parameters with *value*
+ * - switched from bit structs to bit masks
+ * - header file cleaned and integrated
+ * 2008-01-14 Tobias Lorenz <tobias.lorenz@gmx.net>
+ * Version 1.0.2
+ * - hex values are now lower case
+ * - commented USB ID for ADS/Tech moved on todo list
+ * - blacklisted si470x in hid-quirks.c
+ * - rds buffer handling functions integrated into *_work, *_read
+ * - rds_command in si470x_poll exchanged against simple retval
+ * - check for firmware version 15
+ * - code order and prototypes still remain the same
+ * - spacing and bottom of band codes remain the same
+ * 2008-01-16 Tobias Lorenz <tobias.lorenz@gmx.net>
+ * Version 1.0.3
+ * - code reordered to avoid function prototypes
+ * - switch/case defaults are now more user-friendly
+ * - unified comment style
+ * - applied all checkpatch.pl v1.12 suggestions
+ * except the warning about the too long lines with bit comments
+ * - renamed FMRADIO to RADIO to cut line length (checkpatch.pl)
+ * 2008-01-22 Tobias Lorenz <tobias.lorenz@gmx.net>
+ * Version 1.0.4
+ * - avoid poss. locking when doing copy_to_user which may sleep
+ * - RDS is automatically activated on read now
+ * - code cleaned of unnecessary rds_commands
+ * - USB Vendor/Product ID for ADS/Tech FM Radio Receiver verified
+ * (thanks to Guillaume RAMOUSSE)
+ * 2008-01-27 Tobias Lorenz <tobias.lorenz@gmx.net>
+ * Version 1.0.5
+ * - number of seek_retries changed to tune_timeout
+ * - fixed problem with incomplete tune operations by own buffers
+ * - optimization of variables and printf types
+ * - improved error logging
+ * 2008-01-31 Tobias Lorenz <tobias.lorenz@gmx.net>
+ * Oliver Neukum <oliver@neukum.org>
+ * Version 1.0.6
+ * - fixed coverity checker warnings in *_usb_driver_disconnect
+ * - probe()/open() race by correct ordering in probe()
+ * - DMA coherency rules by separate allocation of all buffers
+ * - use of endianness macros
+ * - abuse of spinlock, replaced by mutex
+ * - racy handling of timer in disconnect,
+ * replaced by delayed_work
+ * - racy interruptible_sleep_on(),
+ * replaced with wait_event_interruptible()
+ * - handle signals in read()
+ * 2008-02-08 Tobias Lorenz <tobias.lorenz@gmx.net>
+ * Oliver Neukum <oliver@neukum.org>
+ * Version 1.0.7
+ * - usb autosuspend support
+ * - unplugging fixed
+ * 2008-05-07 Tobias Lorenz <tobias.lorenz@gmx.net>
+ * Version 1.0.8
+ * - hardware frequency seek support
+ * - afc indication
+ * - more safety checks, let si470x_get_freq return errno
+ * - vidioc behavior corrected according to v4l2 spec
+ * 2008-10-20 Alexey Klimov <klimov.linux@gmail.com>
+ * - add support for KWorld USB FM Radio FM700
+ * - blacklisted KWorld radio in hid-core.c and hid-ids.h
+ * 2008-12-03 Mark Lord <mlord@pobox.com>
+ * - add support for DealExtreme USB Radio
+ * 2009-01-31 Bob Ross <pigiron@gmx.com>
+ * - correction of stereo detection/setting
+ * - correction of signal strength indicator scaling
+ * 2009-01-31 Rick Bronson <rick@efn.org>
+ * Tobias Lorenz <tobias.lorenz@gmx.net>
+ * - add LED status output
+ * - get HW/SW version from scratchpad
+ * 2009-06-16 Edouard Lafargue <edouard@lafargue.name>
+ * Version 1.0.10
+ * - add support for interrupt mode for RDS endpoint,
+ * instead of polling.
+ * Improves RDS reception significantly
+ */
+
+
+/* kernel includes */
+#include "radio-si470x.h"
+
+/**************************************************************************
+ * Module Parameters
+ **************************************************************************/
+
+/* Spacing (kHz) */
+/* 0: 200 kHz (USA, Australia) */
+/* 1: 100 kHz (Europe, Japan) */
+/* 2: 50 kHz */
+static unsigned short space = 2;
+module_param(space, ushort, 0444);
+MODULE_PARM_DESC(space, "Spacing: 0=200kHz 1=100kHz *2=50kHz*");
+
+/* De-emphasis */
+/* 0: 75 us (USA) */
+/* 1: 50 us (Europe, Australia, Japan) */
+static unsigned short de = 1;
+module_param(de, ushort, 0444);
+MODULE_PARM_DESC(de, "De-emphasis: 0=75us *1=50us*");
+
+/* Tune timeout */
+static unsigned int tune_timeout = 3000;
+module_param(tune_timeout, uint, 0644);
+MODULE_PARM_DESC(tune_timeout, "Tune timeout: *3000*");
+
+/* Seek timeout */
+static unsigned int seek_timeout = 5000;
+module_param(seek_timeout, uint, 0644);
+MODULE_PARM_DESC(seek_timeout, "Seek timeout: *5000*");
+
+static const struct v4l2_frequency_band bands[] = {
+ {
+ .type = V4L2_TUNER_RADIO,
+ .index = 0,
+ .capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO |
+ V4L2_TUNER_CAP_RDS | V4L2_TUNER_CAP_RDS_BLOCK_IO |
+ V4L2_TUNER_CAP_FREQ_BANDS |
+ V4L2_TUNER_CAP_HWSEEK_BOUNDED |
+ V4L2_TUNER_CAP_HWSEEK_WRAP,
+ .rangelow = 87500 * 16,
+ .rangehigh = 108000 * 16,
+ .modulation = V4L2_BAND_MODULATION_FM,
+ },
+ {
+ .type = V4L2_TUNER_RADIO,
+ .index = 1,
+ .capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO |
+ V4L2_TUNER_CAP_RDS | V4L2_TUNER_CAP_RDS_BLOCK_IO |
+ V4L2_TUNER_CAP_FREQ_BANDS |
+ V4L2_TUNER_CAP_HWSEEK_BOUNDED |
+ V4L2_TUNER_CAP_HWSEEK_WRAP,
+ .rangelow = 76000 * 16,
+ .rangehigh = 108000 * 16,
+ .modulation = V4L2_BAND_MODULATION_FM,
+ },
+ {
+ .type = V4L2_TUNER_RADIO,
+ .index = 2,
+ .capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO |
+ V4L2_TUNER_CAP_RDS | V4L2_TUNER_CAP_RDS_BLOCK_IO |
+ V4L2_TUNER_CAP_FREQ_BANDS |
+ V4L2_TUNER_CAP_HWSEEK_BOUNDED |
+ V4L2_TUNER_CAP_HWSEEK_WRAP,
+ .rangelow = 76000 * 16,
+ .rangehigh = 90000 * 16,
+ .modulation = V4L2_BAND_MODULATION_FM,
+ },
+};
+
+/**************************************************************************
+ * Generic Functions
+ **************************************************************************/
+
+/*
+ * si470x_set_band - set the band
+ */
+static int si470x_set_band(struct si470x_device *radio, int band)
+{
+ if (radio->band == band)
+ return 0;
+
+ radio->band = band;
+ radio->registers[SYSCONFIG2] &= ~SYSCONFIG2_BAND;
+ radio->registers[SYSCONFIG2] |= radio->band << 6;
+ return radio->set_register(radio, SYSCONFIG2);
+}
+
+/*
+ * si470x_set_chan - set the channel
+ */
+static int si470x_set_chan(struct si470x_device *radio, unsigned short chan)
+{
+ int retval;
+ unsigned long time_left;
+ bool timed_out = false;
+
+ retval = radio->get_register(radio, POWERCFG);
+ if (retval)
+ return retval;
+
+ if ((radio->registers[POWERCFG] & (POWERCFG_ENABLE|POWERCFG_DMUTE))
+ != (POWERCFG_ENABLE|POWERCFG_DMUTE)) {
+ return 0;
+ }
+
+ /* start tuning */
+ radio->registers[CHANNEL] &= ~CHANNEL_CHAN;
+ radio->registers[CHANNEL] |= CHANNEL_TUNE | chan;
+ retval = radio->set_register(radio, CHANNEL);
+ if (retval < 0)
+ goto done;
+
+ /* wait till tune operation has completed */
+ reinit_completion(&radio->completion);
+ time_left = wait_for_completion_timeout(&radio->completion,
+ msecs_to_jiffies(tune_timeout));
+ if (time_left == 0)
+ timed_out = true;
+
+ if ((radio->registers[STATUSRSSI] & STATUSRSSI_STC) == 0)
+ dev_warn(&radio->videodev.dev, "tune does not complete\n");
+ if (timed_out)
+ dev_warn(&radio->videodev.dev,
+ "tune timed out after %u ms\n", tune_timeout);
+
+ /* stop tuning */
+ radio->registers[CHANNEL] &= ~CHANNEL_TUNE;
+ retval = radio->set_register(radio, CHANNEL);
+
+done:
+ return retval;
+}
+
+/*
+ * si470x_get_step - get channel spacing
+ */
+static unsigned int si470x_get_step(struct si470x_device *radio)
+{
+ /* Spacing (kHz) */
+ switch ((radio->registers[SYSCONFIG2] & SYSCONFIG2_SPACE) >> 4) {
+ /* 0: 200 kHz (USA, Australia) */
+ case 0:
+ return 200 * 16;
+ /* 1: 100 kHz (Europe, Japan) */
+ case 1:
+ return 100 * 16;
+ /* 2: 50 kHz */
+ default:
+ return 50 * 16;
+ }
+}
+
+
+/*
+ * si470x_get_freq - get the frequency
+ */
+static int si470x_get_freq(struct si470x_device *radio, unsigned int *freq)
+{
+ int chan, retval;
+
+ /* read channel */
+ retval = radio->get_register(radio, READCHAN);
+ chan = radio->registers[READCHAN] & READCHAN_READCHAN;
+
+ /* Frequency (MHz) = Spacing (kHz) x Channel + Bottom of Band (MHz) */
+ *freq = chan * si470x_get_step(radio) + bands[radio->band].rangelow;
+
+ return retval;
+}
+
+
+/*
+ * si470x_set_freq - set the frequency
+ */
+int si470x_set_freq(struct si470x_device *radio, unsigned int freq)
+{
+ unsigned short chan;
+
+ freq = clamp(freq, bands[radio->band].rangelow,
+ bands[radio->band].rangehigh);
+ /* Chan = [ Freq (Mhz) - Bottom of Band (MHz) ] / Spacing (kHz) */
+ chan = (freq - bands[radio->band].rangelow) / si470x_get_step(radio);
+
+ return si470x_set_chan(radio, chan);
+}
+EXPORT_SYMBOL_GPL(si470x_set_freq);
+
+
+/*
+ * si470x_set_seek - set seek
+ */
+static int si470x_set_seek(struct si470x_device *radio,
+ const struct v4l2_hw_freq_seek *seek)
+{
+ int band, retval;
+ unsigned int freq;
+ bool timed_out = false;
+ unsigned long time_left;
+
+ /* set band */
+ if (seek->rangelow || seek->rangehigh) {
+ for (band = 0; band < ARRAY_SIZE(bands); band++) {
+ if (bands[band].rangelow == seek->rangelow &&
+ bands[band].rangehigh == seek->rangehigh)
+ break;
+ }
+ if (band == ARRAY_SIZE(bands))
+ return -EINVAL; /* No matching band found */
+ } else
+ band = 1; /* If nothing is specified seek 76 - 108 Mhz */
+
+ if (radio->band != band) {
+ retval = si470x_get_freq(radio, &freq);
+ if (retval)
+ return retval;
+ retval = si470x_set_band(radio, band);
+ if (retval)
+ return retval;
+ retval = si470x_set_freq(radio, freq);
+ if (retval)
+ return retval;
+ }
+
+ /* start seeking */
+ radio->registers[POWERCFG] |= POWERCFG_SEEK;
+ if (seek->wrap_around)
+ radio->registers[POWERCFG] &= ~POWERCFG_SKMODE;
+ else
+ radio->registers[POWERCFG] |= POWERCFG_SKMODE;
+ if (seek->seek_upward)
+ radio->registers[POWERCFG] |= POWERCFG_SEEKUP;
+ else
+ radio->registers[POWERCFG] &= ~POWERCFG_SEEKUP;
+ retval = radio->set_register(radio, POWERCFG);
+ if (retval < 0)
+ return retval;
+
+ /* wait till tune operation has completed */
+ reinit_completion(&radio->completion);
+ time_left = wait_for_completion_timeout(&radio->completion,
+ msecs_to_jiffies(seek_timeout));
+ if (time_left == 0)
+ timed_out = true;
+
+ if ((radio->registers[STATUSRSSI] & STATUSRSSI_STC) == 0)
+ dev_warn(&radio->videodev.dev, "seek does not complete\n");
+ if (radio->registers[STATUSRSSI] & STATUSRSSI_SF)
+ dev_warn(&radio->videodev.dev,
+ "seek failed / band limit reached\n");
+
+ /* stop seeking */
+ radio->registers[POWERCFG] &= ~POWERCFG_SEEK;
+ retval = radio->set_register(radio, POWERCFG);
+
+ /* try again, if timed out */
+ if (retval == 0 && timed_out)
+ return -ENODATA;
+ return retval;
+}
+
+
+/*
+ * si470x_start - switch on radio
+ */
+int si470x_start(struct si470x_device *radio)
+{
+ int retval;
+
+ /* powercfg */
+ radio->registers[POWERCFG] =
+ POWERCFG_DMUTE | POWERCFG_ENABLE | POWERCFG_RDSM;
+ retval = radio->set_register(radio, POWERCFG);
+ if (retval < 0)
+ goto done;
+
+ /* sysconfig 1 */
+ radio->registers[SYSCONFIG1] |= SYSCONFIG1_RDSIEN | SYSCONFIG1_STCIEN |
+ SYSCONFIG1_RDS;
+ radio->registers[SYSCONFIG1] &= ~SYSCONFIG1_GPIO2;
+ radio->registers[SYSCONFIG1] |= SYSCONFIG1_GPIO2_INT;
+ if (de)
+ radio->registers[SYSCONFIG1] |= SYSCONFIG1_DE;
+ retval = radio->set_register(radio, SYSCONFIG1);
+ if (retval < 0)
+ goto done;
+
+ /* sysconfig 2 */
+ radio->registers[SYSCONFIG2] =
+ (0x1f << 8) | /* SEEKTH */
+ ((radio->band << 6) & SYSCONFIG2_BAND) |/* BAND */
+ ((space << 4) & SYSCONFIG2_SPACE) | /* SPACE */
+ 15; /* VOLUME (max) */
+ retval = radio->set_register(radio, SYSCONFIG2);
+ if (retval < 0)
+ goto done;
+
+ /* reset last channel */
+ retval = si470x_set_chan(radio,
+ radio->registers[CHANNEL] & CHANNEL_CHAN);
+
+done:
+ return retval;
+}
+EXPORT_SYMBOL_GPL(si470x_start);
+
+
+/*
+ * si470x_stop - switch off radio
+ */
+int si470x_stop(struct si470x_device *radio)
+{
+ int retval;
+
+ /* sysconfig 1 */
+ radio->registers[SYSCONFIG1] &= ~SYSCONFIG1_RDS;
+ retval = radio->set_register(radio, SYSCONFIG1);
+ if (retval < 0)
+ goto done;
+
+ /* powercfg */
+ radio->registers[POWERCFG] &= ~POWERCFG_DMUTE;
+ /* POWERCFG_ENABLE has to automatically go low */
+ radio->registers[POWERCFG] |= POWERCFG_ENABLE | POWERCFG_DISABLE;
+ retval = radio->set_register(radio, POWERCFG);
+
+done:
+ return retval;
+}
+EXPORT_SYMBOL_GPL(si470x_stop);
+
+
+/*
+ * si470x_rds_on - switch on rds reception
+ */
+static int si470x_rds_on(struct si470x_device *radio)
+{
+ int retval;
+
+ /* sysconfig 1 */
+ radio->registers[SYSCONFIG1] |= SYSCONFIG1_RDS;
+ retval = radio->set_register(radio, SYSCONFIG1);
+ if (retval < 0)
+ radio->registers[SYSCONFIG1] &= ~SYSCONFIG1_RDS;
+
+ return retval;
+}
+
+
+
+/**************************************************************************
+ * File Operations Interface
+ **************************************************************************/
+
+/*
+ * si470x_fops_read - read RDS data
+ */
+static ssize_t si470x_fops_read(struct file *file, char __user *buf,
+ size_t count, loff_t *ppos)
+{
+ struct si470x_device *radio = video_drvdata(file);
+ int retval = 0;
+ unsigned int block_count = 0;
+
+ /* switch on rds reception */
+ if ((radio->registers[SYSCONFIG1] & SYSCONFIG1_RDS) == 0)
+ si470x_rds_on(radio);
+
+ /* block if no new data available */
+ while (radio->wr_index == radio->rd_index) {
+ if (file->f_flags & O_NONBLOCK) {
+ retval = -EWOULDBLOCK;
+ goto done;
+ }
+ if (wait_event_interruptible(radio->read_queue,
+ radio->wr_index != radio->rd_index) < 0) {
+ retval = -EINTR;
+ goto done;
+ }
+ }
+
+ /* calculate block count from byte count */
+ count /= 3;
+
+ /* copy RDS block out of internal buffer and to user buffer */
+ while (block_count < count) {
+ if (radio->rd_index == radio->wr_index)
+ break;
+
+ /* always transfer rds complete blocks */
+ if (copy_to_user(buf, &radio->buffer[radio->rd_index], 3))
+ /* retval = -EFAULT; */
+ break;
+
+ /* increment and wrap read pointer */
+ radio->rd_index += 3;
+ if (radio->rd_index >= radio->buf_size)
+ radio->rd_index = 0;
+
+ /* increment counters */
+ block_count++;
+ buf += 3;
+ retval += 3;
+ }
+
+done:
+ return retval;
+}
+
+
+/*
+ * si470x_fops_poll - poll RDS data
+ */
+static __poll_t si470x_fops_poll(struct file *file,
+ struct poll_table_struct *pts)
+{
+ struct si470x_device *radio = video_drvdata(file);
+ __poll_t req_events = poll_requested_events(pts);
+ __poll_t retval = v4l2_ctrl_poll(file, pts);
+
+ if (req_events & (EPOLLIN | EPOLLRDNORM)) {
+ /* switch on rds reception */
+ if ((radio->registers[SYSCONFIG1] & SYSCONFIG1_RDS) == 0)
+ si470x_rds_on(radio);
+
+ poll_wait(file, &radio->read_queue, pts);
+
+ if (radio->rd_index != radio->wr_index)
+ retval |= EPOLLIN | EPOLLRDNORM;
+ }
+
+ return retval;
+}
+
+
+static int si470x_fops_open(struct file *file)
+{
+ struct si470x_device *radio = video_drvdata(file);
+
+ return radio->fops_open(file);
+}
+
+
+/*
+ * si470x_fops_release - file release
+ */
+static int si470x_fops_release(struct file *file)
+{
+ struct si470x_device *radio = video_drvdata(file);
+
+ return radio->fops_release(file);
+}
+
+
+/*
+ * si470x_fops - file operations interface
+ */
+static const struct v4l2_file_operations si470x_fops = {
+ .owner = THIS_MODULE,
+ .read = si470x_fops_read,
+ .poll = si470x_fops_poll,
+ .unlocked_ioctl = video_ioctl2,
+ .open = si470x_fops_open,
+ .release = si470x_fops_release,
+};
+
+
+
+/**************************************************************************
+ * Video4Linux Interface
+ **************************************************************************/
+
+
+static int si470x_s_ctrl(struct v4l2_ctrl *ctrl)
+{
+ struct si470x_device *radio =
+ container_of(ctrl->handler, struct si470x_device, hdl);
+
+ switch (ctrl->id) {
+ case V4L2_CID_AUDIO_VOLUME:
+ radio->registers[SYSCONFIG2] &= ~SYSCONFIG2_VOLUME;
+ radio->registers[SYSCONFIG2] |= ctrl->val;
+ return radio->set_register(radio, SYSCONFIG2);
+ case V4L2_CID_AUDIO_MUTE:
+ if (ctrl->val)
+ radio->registers[POWERCFG] &= ~POWERCFG_DMUTE;
+ else
+ radio->registers[POWERCFG] |= POWERCFG_DMUTE;
+ return radio->set_register(radio, POWERCFG);
+ default:
+ return -EINVAL;
+ }
+}
+
+
+/*
+ * si470x_vidioc_g_tuner - get tuner attributes
+ */
+static int si470x_vidioc_g_tuner(struct file *file, void *priv,
+ struct v4l2_tuner *tuner)
+{
+ struct si470x_device *radio = video_drvdata(file);
+ int retval = 0;
+
+ if (tuner->index != 0)
+ return -EINVAL;
+
+ if (!radio->status_rssi_auto_update) {
+ retval = radio->get_register(radio, STATUSRSSI);
+ if (retval < 0)
+ return retval;
+ }
+
+ /* driver constants */
+ strscpy(tuner->name, "FM", sizeof(tuner->name));
+ tuner->type = V4L2_TUNER_RADIO;
+ tuner->capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO |
+ V4L2_TUNER_CAP_RDS | V4L2_TUNER_CAP_RDS_BLOCK_IO |
+ V4L2_TUNER_CAP_HWSEEK_BOUNDED |
+ V4L2_TUNER_CAP_HWSEEK_WRAP;
+ tuner->rangelow = 76 * FREQ_MUL;
+ tuner->rangehigh = 108 * FREQ_MUL;
+
+ /* stereo indicator == stereo (instead of mono) */
+ if ((radio->registers[STATUSRSSI] & STATUSRSSI_ST) == 0)
+ tuner->rxsubchans = V4L2_TUNER_SUB_MONO;
+ else
+ tuner->rxsubchans = V4L2_TUNER_SUB_STEREO;
+ /* If there is a reliable method of detecting an RDS channel,
+ then this code should check for that before setting this
+ RDS subchannel. */
+ tuner->rxsubchans |= V4L2_TUNER_SUB_RDS;
+
+ /* mono/stereo selector */
+ if ((radio->registers[POWERCFG] & POWERCFG_MONO) == 0)
+ tuner->audmode = V4L2_TUNER_MODE_STEREO;
+ else
+ tuner->audmode = V4L2_TUNER_MODE_MONO;
+
+ /* min is worst, max is best; signal:0..0xffff; rssi: 0..0xff */
+ /* measured in units of dbµV in 1 db increments (max at ~75 dbµV) */
+ tuner->signal = (radio->registers[STATUSRSSI] & STATUSRSSI_RSSI);
+ /* the ideal factor is 0xffff/75 = 873,8 */
+ tuner->signal = (tuner->signal * 873) + (8 * tuner->signal / 10);
+ if (tuner->signal > 0xffff)
+ tuner->signal = 0xffff;
+
+ /* automatic frequency control: -1: freq to low, 1 freq to high */
+ /* AFCRL does only indicate that freq. differs, not if too low/high */
+ tuner->afc = (radio->registers[STATUSRSSI] & STATUSRSSI_AFCRL) ? 1 : 0;
+
+ return retval;
+}
+
+
+/*
+ * si470x_vidioc_s_tuner - set tuner attributes
+ */
+static int si470x_vidioc_s_tuner(struct file *file, void *priv,
+ const struct v4l2_tuner *tuner)
+{
+ struct si470x_device *radio = video_drvdata(file);
+
+ if (tuner->index != 0)
+ return -EINVAL;
+
+ /* mono/stereo selector */
+ switch (tuner->audmode) {
+ case V4L2_TUNER_MODE_MONO:
+ radio->registers[POWERCFG] |= POWERCFG_MONO; /* force mono */
+ break;
+ case V4L2_TUNER_MODE_STEREO:
+ default:
+ radio->registers[POWERCFG] &= ~POWERCFG_MONO; /* try stereo */
+ break;
+ }
+
+ return radio->set_register(radio, POWERCFG);
+}
+
+
+/*
+ * si470x_vidioc_g_frequency - get tuner or modulator radio frequency
+ */
+static int si470x_vidioc_g_frequency(struct file *file, void *priv,
+ struct v4l2_frequency *freq)
+{
+ struct si470x_device *radio = video_drvdata(file);
+
+ if (freq->tuner != 0)
+ return -EINVAL;
+
+ freq->type = V4L2_TUNER_RADIO;
+ return si470x_get_freq(radio, &freq->frequency);
+}
+
+
+/*
+ * si470x_vidioc_s_frequency - set tuner or modulator radio frequency
+ */
+static int si470x_vidioc_s_frequency(struct file *file, void *priv,
+ const struct v4l2_frequency *freq)
+{
+ struct si470x_device *radio = video_drvdata(file);
+ int retval;
+
+ if (freq->tuner != 0)
+ return -EINVAL;
+
+ if (freq->frequency < bands[radio->band].rangelow ||
+ freq->frequency > bands[radio->band].rangehigh) {
+ /* Switch to band 1 which covers everything we support */
+ retval = si470x_set_band(radio, 1);
+ if (retval)
+ return retval;
+ }
+ return si470x_set_freq(radio, freq->frequency);
+}
+
+
+/*
+ * si470x_vidioc_s_hw_freq_seek - set hardware frequency seek
+ */
+static int si470x_vidioc_s_hw_freq_seek(struct file *file, void *priv,
+ const struct v4l2_hw_freq_seek *seek)
+{
+ struct si470x_device *radio = video_drvdata(file);
+
+ if (seek->tuner != 0)
+ return -EINVAL;
+
+ if (file->f_flags & O_NONBLOCK)
+ return -EWOULDBLOCK;
+
+ return si470x_set_seek(radio, seek);
+}
+
+/*
+ * si470x_vidioc_enum_freq_bands - enumerate supported bands
+ */
+static int si470x_vidioc_enum_freq_bands(struct file *file, void *priv,
+ struct v4l2_frequency_band *band)
+{
+ if (band->tuner != 0)
+ return -EINVAL;
+ if (band->index >= ARRAY_SIZE(bands))
+ return -EINVAL;
+ *band = bands[band->index];
+ return 0;
+}
+
+const struct v4l2_ctrl_ops si470x_ctrl_ops = {
+ .s_ctrl = si470x_s_ctrl,
+};
+EXPORT_SYMBOL_GPL(si470x_ctrl_ops);
+
+static int si470x_vidioc_querycap(struct file *file, void *priv,
+ struct v4l2_capability *capability)
+{
+ struct si470x_device *radio = video_drvdata(file);
+
+ return radio->vidioc_querycap(file, priv, capability);
+};
+
+/*
+ * si470x_ioctl_ops - video device ioctl operations
+ */
+static const struct v4l2_ioctl_ops si470x_ioctl_ops = {
+ .vidioc_querycap = si470x_vidioc_querycap,
+ .vidioc_g_tuner = si470x_vidioc_g_tuner,
+ .vidioc_s_tuner = si470x_vidioc_s_tuner,
+ .vidioc_g_frequency = si470x_vidioc_g_frequency,
+ .vidioc_s_frequency = si470x_vidioc_s_frequency,
+ .vidioc_s_hw_freq_seek = si470x_vidioc_s_hw_freq_seek,
+ .vidioc_enum_freq_bands = si470x_vidioc_enum_freq_bands,
+ .vidioc_subscribe_event = v4l2_ctrl_subscribe_event,
+ .vidioc_unsubscribe_event = v4l2_event_unsubscribe,
+};
+
+
+/*
+ * si470x_viddev_template - video device interface
+ */
+const struct video_device si470x_viddev_template = {
+ .fops = &si470x_fops,
+ .name = DRIVER_NAME,
+ .release = video_device_release_empty,
+ .ioctl_ops = &si470x_ioctl_ops,
+};
+EXPORT_SYMBOL_GPL(si470x_viddev_template);
+
+MODULE_DESCRIPTION("Core radio driver for Si470x FM Radio Receivers");
+MODULE_LICENSE("GPL");
diff --git a/drivers/media/radio/si470x/radio-si470x-i2c.c b/drivers/media/radio/si470x/radio-si470x-i2c.c
new file mode 100644
index 000000000..a1e570af9
--- /dev/null
+++ b/drivers/media/radio/si470x/radio-si470x-i2c.c
@@ -0,0 +1,545 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * drivers/media/radio/si470x/radio-si470x-i2c.c
+ *
+ * I2C driver for radios with Silicon Labs Si470x FM Radio Receivers
+ *
+ * Copyright (c) 2009 Samsung Electronics Co.Ltd
+ * Author: Joonyoung Shim <jy0922.shim@samsung.com>
+ */
+
+
+/* driver definitions */
+#define DRIVER_AUTHOR "Joonyoung Shim <jy0922.shim@samsung.com>"
+#define DRIVER_CARD "Silicon Labs Si470x FM Radio"
+#define DRIVER_DESC "I2C radio driver for Si470x FM Radio Receivers"
+#define DRIVER_VERSION "1.0.2"
+
+/* kernel includes */
+#include <linux/i2c.h>
+#include <linux/slab.h>
+#include <linux/delay.h>
+#include <linux/gpio/consumer.h>
+#include <linux/interrupt.h>
+
+#include "radio-si470x.h"
+
+
+/* I2C Device ID List */
+static const struct i2c_device_id si470x_i2c_id[] = {
+ /* Generic Entry */
+ { .name = "si470x" },
+ /* Terminating entry */
+ { }
+};
+MODULE_DEVICE_TABLE(i2c, si470x_i2c_id);
+
+
+/**************************************************************************
+ * Module Parameters
+ **************************************************************************/
+
+/* Radio Nr */
+static int radio_nr = -1;
+module_param(radio_nr, int, 0444);
+MODULE_PARM_DESC(radio_nr, "Radio Nr");
+
+/* RDS buffer blocks */
+static unsigned int rds_buf = 100;
+module_param(rds_buf, uint, 0444);
+MODULE_PARM_DESC(rds_buf, "RDS buffer entries: *100*");
+
+/* RDS maximum block errors */
+static unsigned short max_rds_errors = 1;
+/* 0 means 0 errors requiring correction */
+/* 1 means 1-2 errors requiring correction (used by original USBRadio.exe) */
+/* 2 means 3-5 errors requiring correction */
+/* 3 means 6+ errors or errors in checkword, correction not possible */
+module_param(max_rds_errors, ushort, 0644);
+MODULE_PARM_DESC(max_rds_errors, "RDS maximum block errors: *1*");
+
+
+
+/**************************************************************************
+ * I2C Definitions
+ **************************************************************************/
+
+/* Write starts with the upper byte of register 0x02 */
+#define WRITE_REG_NUM 8
+#define WRITE_INDEX(i) (i + 0x02)
+
+/* Read starts with the upper byte of register 0x0a */
+#define READ_REG_NUM RADIO_REGISTER_NUM
+#define READ_INDEX(i) ((i + RADIO_REGISTER_NUM - 0x0a) % READ_REG_NUM)
+
+
+
+/**************************************************************************
+ * General Driver Functions - REGISTERs
+ **************************************************************************/
+
+/*
+ * si470x_get_register - read register
+ */
+static int si470x_get_register(struct si470x_device *radio, int regnr)
+{
+ __be16 buf[READ_REG_NUM];
+ struct i2c_msg msgs[1] = {
+ {
+ .addr = radio->client->addr,
+ .flags = I2C_M_RD,
+ .len = sizeof(u16) * READ_REG_NUM,
+ .buf = (void *)buf
+ },
+ };
+
+ if (i2c_transfer(radio->client->adapter, msgs, 1) != 1)
+ return -EIO;
+
+ radio->registers[regnr] = __be16_to_cpu(buf[READ_INDEX(regnr)]);
+
+ return 0;
+}
+
+
+/*
+ * si470x_set_register - write register
+ */
+static int si470x_set_register(struct si470x_device *radio, int regnr)
+{
+ int i;
+ __be16 buf[WRITE_REG_NUM];
+ struct i2c_msg msgs[1] = {
+ {
+ .addr = radio->client->addr,
+ .len = sizeof(u16) * WRITE_REG_NUM,
+ .buf = (void *)buf
+ },
+ };
+
+ for (i = 0; i < WRITE_REG_NUM; i++)
+ buf[i] = __cpu_to_be16(radio->registers[WRITE_INDEX(i)]);
+
+ if (i2c_transfer(radio->client->adapter, msgs, 1) != 1)
+ return -EIO;
+
+ return 0;
+}
+
+
+
+/**************************************************************************
+ * General Driver Functions - ENTIRE REGISTERS
+ **************************************************************************/
+
+/*
+ * si470x_get_all_registers - read entire registers
+ */
+static int si470x_get_all_registers(struct si470x_device *radio)
+{
+ int i;
+ __be16 buf[READ_REG_NUM];
+ struct i2c_msg msgs[1] = {
+ {
+ .addr = radio->client->addr,
+ .flags = I2C_M_RD,
+ .len = sizeof(u16) * READ_REG_NUM,
+ .buf = (void *)buf
+ },
+ };
+
+ if (i2c_transfer(radio->client->adapter, msgs, 1) != 1)
+ return -EIO;
+
+ for (i = 0; i < READ_REG_NUM; i++)
+ radio->registers[i] = __be16_to_cpu(buf[READ_INDEX(i)]);
+
+ return 0;
+}
+
+
+
+/**************************************************************************
+ * File Operations Interface
+ **************************************************************************/
+
+/*
+ * si470x_fops_open - file open
+ */
+static int si470x_fops_open(struct file *file)
+{
+ struct si470x_device *radio = video_drvdata(file);
+ int retval = v4l2_fh_open(file);
+
+ if (retval)
+ return retval;
+
+ if (v4l2_fh_is_singular_file(file)) {
+ /* start radio */
+ retval = si470x_start(radio);
+ if (retval < 0)
+ goto done;
+
+ /* enable RDS / STC interrupt */
+ radio->registers[SYSCONFIG1] |= SYSCONFIG1_RDSIEN;
+ radio->registers[SYSCONFIG1] |= SYSCONFIG1_STCIEN;
+ radio->registers[SYSCONFIG1] &= ~SYSCONFIG1_GPIO2;
+ radio->registers[SYSCONFIG1] |= 0x1 << 2;
+ retval = si470x_set_register(radio, SYSCONFIG1);
+ }
+
+done:
+ if (retval)
+ v4l2_fh_release(file);
+ return retval;
+}
+
+
+/*
+ * si470x_fops_release - file release
+ */
+static int si470x_fops_release(struct file *file)
+{
+ struct si470x_device *radio = video_drvdata(file);
+
+ if (v4l2_fh_is_singular_file(file))
+ /* stop radio */
+ si470x_stop(radio);
+
+ return v4l2_fh_release(file);
+}
+
+
+
+/**************************************************************************
+ * Video4Linux Interface
+ **************************************************************************/
+
+/*
+ * si470x_vidioc_querycap - query device capabilities
+ */
+static int si470x_vidioc_querycap(struct file *file, void *priv,
+ struct v4l2_capability *capability)
+{
+ strscpy(capability->driver, DRIVER_NAME, sizeof(capability->driver));
+ strscpy(capability->card, DRIVER_CARD, sizeof(capability->card));
+ return 0;
+}
+
+
+
+/**************************************************************************
+ * I2C Interface
+ **************************************************************************/
+
+/*
+ * si470x_i2c_interrupt - interrupt handler
+ */
+static irqreturn_t si470x_i2c_interrupt(int irq, void *dev_id)
+{
+ struct si470x_device *radio = dev_id;
+ unsigned char regnr;
+ unsigned char blocknum;
+ unsigned short bler; /* rds block errors */
+ unsigned short rds;
+ unsigned char tmpbuf[3];
+ int retval = 0;
+
+ /* check Seek/Tune Complete */
+ retval = si470x_get_register(radio, STATUSRSSI);
+ if (retval < 0)
+ goto end;
+
+ if (radio->registers[STATUSRSSI] & STATUSRSSI_STC)
+ complete(&radio->completion);
+
+ /* safety checks */
+ if ((radio->registers[SYSCONFIG1] & SYSCONFIG1_RDS) == 0)
+ goto end;
+
+ /* Update RDS registers */
+ for (regnr = 1; regnr < RDS_REGISTER_NUM; regnr++) {
+ retval = si470x_get_register(radio, STATUSRSSI + regnr);
+ if (retval < 0)
+ goto end;
+ }
+
+ /* get rds blocks */
+ if ((radio->registers[STATUSRSSI] & STATUSRSSI_RDSR) == 0)
+ /* No RDS group ready, better luck next time */
+ goto end;
+
+ for (blocknum = 0; blocknum < 4; blocknum++) {
+ switch (blocknum) {
+ default:
+ bler = (radio->registers[STATUSRSSI] &
+ STATUSRSSI_BLERA) >> 9;
+ rds = radio->registers[RDSA];
+ break;
+ case 1:
+ bler = (radio->registers[READCHAN] &
+ READCHAN_BLERB) >> 14;
+ rds = radio->registers[RDSB];
+ break;
+ case 2:
+ bler = (radio->registers[READCHAN] &
+ READCHAN_BLERC) >> 12;
+ rds = radio->registers[RDSC];
+ break;
+ case 3:
+ bler = (radio->registers[READCHAN] &
+ READCHAN_BLERD) >> 10;
+ rds = radio->registers[RDSD];
+ break;
+ }
+
+ /* Fill the V4L2 RDS buffer */
+ put_unaligned_le16(rds, &tmpbuf);
+ tmpbuf[2] = blocknum; /* offset name */
+ tmpbuf[2] |= blocknum << 3; /* received offset */
+ if (bler > max_rds_errors)
+ tmpbuf[2] |= 0x80; /* uncorrectable errors */
+ else if (bler > 0)
+ tmpbuf[2] |= 0x40; /* corrected error(s) */
+
+ /* copy RDS block to internal buffer */
+ memcpy(&radio->buffer[radio->wr_index], &tmpbuf, 3);
+ radio->wr_index += 3;
+
+ /* wrap write pointer */
+ if (radio->wr_index >= radio->buf_size)
+ radio->wr_index = 0;
+
+ /* check for overflow */
+ if (radio->wr_index == radio->rd_index) {
+ /* increment and wrap read pointer */
+ radio->rd_index += 3;
+ if (radio->rd_index >= radio->buf_size)
+ radio->rd_index = 0;
+ }
+ }
+
+ if (radio->wr_index != radio->rd_index)
+ wake_up_interruptible(&radio->read_queue);
+
+end:
+ return IRQ_HANDLED;
+}
+
+
+/*
+ * si470x_i2c_probe - probe for the device
+ */
+static int si470x_i2c_probe(struct i2c_client *client)
+{
+ struct si470x_device *radio;
+ int retval = 0;
+
+ /* private data allocation and initialization */
+ radio = devm_kzalloc(&client->dev, sizeof(*radio), GFP_KERNEL);
+ if (!radio) {
+ retval = -ENOMEM;
+ goto err_initial;
+ }
+
+ radio->client = client;
+ radio->band = 1; /* Default to 76 - 108 MHz */
+ mutex_init(&radio->lock);
+ init_completion(&radio->completion);
+
+ radio->get_register = si470x_get_register;
+ radio->set_register = si470x_set_register;
+ radio->fops_open = si470x_fops_open;
+ radio->fops_release = si470x_fops_release;
+ radio->vidioc_querycap = si470x_vidioc_querycap;
+
+ retval = v4l2_device_register(&client->dev, &radio->v4l2_dev);
+ if (retval < 0) {
+ dev_err(&client->dev, "couldn't register v4l2_device\n");
+ goto err_initial;
+ }
+
+ v4l2_ctrl_handler_init(&radio->hdl, 2);
+ v4l2_ctrl_new_std(&radio->hdl, &si470x_ctrl_ops,
+ V4L2_CID_AUDIO_MUTE, 0, 1, 1, 1);
+ v4l2_ctrl_new_std(&radio->hdl, &si470x_ctrl_ops,
+ V4L2_CID_AUDIO_VOLUME, 0, 15, 1, 15);
+ if (radio->hdl.error) {
+ retval = radio->hdl.error;
+ dev_err(&client->dev, "couldn't register control\n");
+ goto err_all;
+ }
+
+ /* video device initialization */
+ radio->videodev = si470x_viddev_template;
+ radio->videodev.ctrl_handler = &radio->hdl;
+ radio->videodev.lock = &radio->lock;
+ radio->videodev.v4l2_dev = &radio->v4l2_dev;
+ radio->videodev.release = video_device_release_empty;
+ radio->videodev.device_caps =
+ V4L2_CAP_HW_FREQ_SEEK | V4L2_CAP_READWRITE | V4L2_CAP_TUNER |
+ V4L2_CAP_RADIO | V4L2_CAP_RDS_CAPTURE;
+ video_set_drvdata(&radio->videodev, radio);
+
+ radio->gpio_reset = devm_gpiod_get_optional(&client->dev, "reset",
+ GPIOD_OUT_LOW);
+ if (IS_ERR(radio->gpio_reset)) {
+ retval = PTR_ERR(radio->gpio_reset);
+ dev_err(&client->dev, "Failed to request gpio: %d\n", retval);
+ goto err_all;
+ }
+
+ if (radio->gpio_reset)
+ gpiod_set_value(radio->gpio_reset, 1);
+
+ /* power up : need 110ms */
+ radio->registers[POWERCFG] = POWERCFG_ENABLE;
+ if (si470x_set_register(radio, POWERCFG) < 0) {
+ retval = -EIO;
+ goto err_all;
+ }
+ msleep(110);
+
+ /* get device and chip versions */
+ if (si470x_get_all_registers(radio) < 0) {
+ retval = -EIO;
+ goto err_all;
+ }
+ dev_info(&client->dev, "DeviceID=0x%4.4hx ChipID=0x%4.4hx\n",
+ radio->registers[DEVICEID], radio->registers[SI_CHIPID]);
+ if ((radio->registers[SI_CHIPID] & SI_CHIPID_FIRMWARE) < RADIO_FW_VERSION) {
+ dev_warn(&client->dev,
+ "This driver is known to work with firmware version %u, but the device has firmware version %u.\n"
+ "If you have some trouble using this driver, please report to V4L ML at linux-media@vger.kernel.org\n",
+ RADIO_FW_VERSION,
+ radio->registers[SI_CHIPID] & SI_CHIPID_FIRMWARE);
+ }
+
+ /* set initial frequency */
+ si470x_set_freq(radio, 87.5 * FREQ_MUL); /* available in all regions */
+
+ /* rds buffer allocation */
+ radio->buf_size = rds_buf * 3;
+ radio->buffer = devm_kmalloc(&client->dev, radio->buf_size, GFP_KERNEL);
+ if (!radio->buffer) {
+ retval = -EIO;
+ goto err_all;
+ }
+
+ /* rds buffer configuration */
+ radio->wr_index = 0;
+ radio->rd_index = 0;
+ init_waitqueue_head(&radio->read_queue);
+
+ retval = devm_request_threaded_irq(&client->dev, client->irq, NULL,
+ si470x_i2c_interrupt,
+ IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
+ DRIVER_NAME, radio);
+ if (retval) {
+ dev_err(&client->dev, "Failed to register interrupt\n");
+ goto err_all;
+ }
+
+ /* register video device */
+ retval = video_register_device(&radio->videodev, VFL_TYPE_RADIO,
+ radio_nr);
+ if (retval) {
+ dev_warn(&client->dev, "Could not register video device\n");
+ goto err_all;
+ }
+ i2c_set_clientdata(client, radio);
+
+ return 0;
+err_all:
+ v4l2_ctrl_handler_free(&radio->hdl);
+ v4l2_device_unregister(&radio->v4l2_dev);
+err_initial:
+ return retval;
+}
+
+
+/*
+ * si470x_i2c_remove - remove the device
+ */
+static void si470x_i2c_remove(struct i2c_client *client)
+{
+ struct si470x_device *radio = i2c_get_clientdata(client);
+
+ video_unregister_device(&radio->videodev);
+
+ if (radio->gpio_reset)
+ gpiod_set_value(radio->gpio_reset, 0);
+
+ v4l2_ctrl_handler_free(&radio->hdl);
+ v4l2_device_unregister(&radio->v4l2_dev);
+}
+
+
+#ifdef CONFIG_PM_SLEEP
+/*
+ * si470x_i2c_suspend - suspend the device
+ */
+static int si470x_i2c_suspend(struct device *dev)
+{
+ struct i2c_client *client = to_i2c_client(dev);
+ struct si470x_device *radio = i2c_get_clientdata(client);
+
+ /* power down */
+ radio->registers[POWERCFG] |= POWERCFG_DISABLE;
+ if (si470x_set_register(radio, POWERCFG) < 0)
+ return -EIO;
+
+ return 0;
+}
+
+
+/*
+ * si470x_i2c_resume - resume the device
+ */
+static int si470x_i2c_resume(struct device *dev)
+{
+ struct i2c_client *client = to_i2c_client(dev);
+ struct si470x_device *radio = i2c_get_clientdata(client);
+
+ /* power up : need 110ms */
+ radio->registers[POWERCFG] |= POWERCFG_ENABLE;
+ if (si470x_set_register(radio, POWERCFG) < 0)
+ return -EIO;
+ msleep(110);
+
+ return 0;
+}
+
+static SIMPLE_DEV_PM_OPS(si470x_i2c_pm, si470x_i2c_suspend, si470x_i2c_resume);
+#endif
+
+#if IS_ENABLED(CONFIG_OF)
+static const struct of_device_id si470x_of_match[] = {
+ { .compatible = "silabs,si470x" },
+ { },
+};
+MODULE_DEVICE_TABLE(of, si470x_of_match);
+#endif
+
+/*
+ * si470x_i2c_driver - i2c driver interface
+ */
+static struct i2c_driver si470x_i2c_driver = {
+ .driver = {
+ .name = "si470x",
+ .of_match_table = of_match_ptr(si470x_of_match),
+#ifdef CONFIG_PM_SLEEP
+ .pm = &si470x_i2c_pm,
+#endif
+ },
+ .probe = si470x_i2c_probe,
+ .remove = si470x_i2c_remove,
+ .id_table = si470x_i2c_id,
+};
+
+module_i2c_driver(si470x_i2c_driver);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR(DRIVER_AUTHOR);
+MODULE_DESCRIPTION(DRIVER_DESC);
+MODULE_VERSION(DRIVER_VERSION);
diff --git a/drivers/media/radio/si470x/radio-si470x-usb.c b/drivers/media/radio/si470x/radio-si470x-usb.c
new file mode 100644
index 000000000..869b1e7e3
--- /dev/null
+++ b/drivers/media/radio/si470x/radio-si470x-usb.c
@@ -0,0 +1,849 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * drivers/media/radio/si470x/radio-si470x-usb.c
+ *
+ * USB driver for radios with Silicon Labs Si470x FM Radio Receivers
+ *
+ * Copyright (c) 2009 Tobias Lorenz <tobias.lorenz@gmx.net>
+ */
+
+
+/*
+ * ToDo:
+ * - add firmware download/update support
+ */
+
+
+/* driver definitions */
+#define DRIVER_AUTHOR "Tobias Lorenz <tobias.lorenz@gmx.net>"
+#define DRIVER_CARD "Silicon Labs Si470x FM Radio"
+#define DRIVER_DESC "USB radio driver for Si470x FM Radio Receivers"
+#define DRIVER_VERSION "1.0.10"
+
+/* kernel includes */
+#include <linux/usb.h>
+#include <linux/hid.h>
+#include <linux/slab.h>
+
+#include "radio-si470x.h"
+
+
+/* USB Device ID List */
+static const struct usb_device_id si470x_usb_driver_id_table[] = {
+ /* Silicon Labs USB FM Radio Reference Design */
+ { USB_DEVICE_AND_INTERFACE_INFO(0x10c4, 0x818a, USB_CLASS_HID, 0, 0) },
+ /* ADS/Tech FM Radio Receiver (formerly Instant FM Music) */
+ { USB_DEVICE_AND_INTERFACE_INFO(0x06e1, 0xa155, USB_CLASS_HID, 0, 0) },
+ /* KWorld USB FM Radio SnapMusic Mobile 700 (FM700) */
+ { USB_DEVICE_AND_INTERFACE_INFO(0x1b80, 0xd700, USB_CLASS_HID, 0, 0) },
+ /* Sanei Electric, Inc. FM USB Radio (sold as DealExtreme.com PCear) */
+ { USB_DEVICE_AND_INTERFACE_INFO(0x10c5, 0x819a, USB_CLASS_HID, 0, 0) },
+ /* Axentia ALERT FM USB Receiver */
+ { USB_DEVICE_AND_INTERFACE_INFO(0x12cf, 0x7111, USB_CLASS_HID, 0, 0) },
+ /* Terminating entry */
+ { }
+};
+MODULE_DEVICE_TABLE(usb, si470x_usb_driver_id_table);
+
+
+
+/**************************************************************************
+ * Module Parameters
+ **************************************************************************/
+
+/* Radio Nr */
+static int radio_nr = -1;
+module_param(radio_nr, int, 0444);
+MODULE_PARM_DESC(radio_nr, "Radio Nr");
+
+/* USB timeout */
+static unsigned int usb_timeout = 500;
+module_param(usb_timeout, uint, 0644);
+MODULE_PARM_DESC(usb_timeout, "USB timeout (ms): *500*");
+
+/* RDS buffer blocks */
+static unsigned int rds_buf = 100;
+module_param(rds_buf, uint, 0444);
+MODULE_PARM_DESC(rds_buf, "RDS buffer entries: *100*");
+
+/* RDS maximum block errors */
+static unsigned short max_rds_errors = 1;
+/* 0 means 0 errors requiring correction */
+/* 1 means 1-2 errors requiring correction (used by original USBRadio.exe) */
+/* 2 means 3-5 errors requiring correction */
+/* 3 means 6+ errors or errors in checkword, correction not possible */
+module_param(max_rds_errors, ushort, 0644);
+MODULE_PARM_DESC(max_rds_errors, "RDS maximum block errors: *1*");
+
+
+
+/**************************************************************************
+ * USB HID Reports
+ **************************************************************************/
+
+/* Reports 1-16 give direct read/write access to the 16 Si470x registers */
+/* with the (REPORT_ID - 1) corresponding to the register address across USB */
+/* endpoint 0 using GET_REPORT and SET_REPORT */
+#define REGISTER_REPORT_SIZE (RADIO_REGISTER_SIZE + 1)
+#define REGISTER_REPORT(reg) ((reg) + 1)
+
+/* Report 17 gives direct read/write access to the entire Si470x register */
+/* map across endpoint 0 using GET_REPORT and SET_REPORT */
+#define ENTIRE_REPORT_SIZE (RADIO_REGISTER_NUM * RADIO_REGISTER_SIZE + 1)
+#define ENTIRE_REPORT 17
+
+/* Report 18 is used to send the lowest 6 Si470x registers up the HID */
+/* interrupt endpoint 1 to Windows every 20 milliseconds for status */
+#define RDS_REPORT_SIZE (RDS_REGISTER_NUM * RADIO_REGISTER_SIZE + 1)
+#define RDS_REPORT 18
+
+/* Report 19: LED state */
+#define LED_REPORT_SIZE 3
+#define LED_REPORT 19
+
+/* Report 19: stream */
+#define STREAM_REPORT_SIZE 3
+#define STREAM_REPORT 19
+
+/* Report 20: scratch */
+#define SCRATCH_PAGE_SIZE 63
+#define SCRATCH_REPORT_SIZE (SCRATCH_PAGE_SIZE + 1)
+#define SCRATCH_REPORT 20
+
+/* Reports 19-22: flash upgrade of the C8051F321 */
+#define WRITE_REPORT_SIZE 4
+#define WRITE_REPORT 19
+#define FLASH_REPORT_SIZE 64
+#define FLASH_REPORT 20
+#define CRC_REPORT_SIZE 3
+#define CRC_REPORT 21
+#define RESPONSE_REPORT_SIZE 2
+#define RESPONSE_REPORT 22
+
+/* Report 23: currently unused, but can accept 60 byte reports on the HID */
+/* interrupt out endpoint 2 every 1 millisecond */
+#define UNUSED_REPORT 23
+
+#define MAX_REPORT_SIZE 64
+
+
+
+/**************************************************************************
+ * Software/Hardware Versions from Scratch Page
+ **************************************************************************/
+#define RADIO_HW_VERSION 1
+
+
+
+/**************************************************************************
+ * LED State Definitions
+ **************************************************************************/
+#define LED_COMMAND 0x35
+
+#define NO_CHANGE_LED 0x00
+#define ALL_COLOR_LED 0x01 /* streaming state */
+#define BLINK_GREEN_LED 0x02 /* connect state */
+#define BLINK_RED_LED 0x04
+#define BLINK_ORANGE_LED 0x10 /* disconnect state */
+#define SOLID_GREEN_LED 0x20 /* tuning/seeking state */
+#define SOLID_RED_LED 0x40 /* bootload state */
+#define SOLID_ORANGE_LED 0x80
+
+
+
+/**************************************************************************
+ * Stream State Definitions
+ **************************************************************************/
+#define STREAM_COMMAND 0x36
+#define STREAM_VIDPID 0x00
+#define STREAM_AUDIO 0xff
+
+
+
+/**************************************************************************
+ * Bootloader / Flash Commands
+ **************************************************************************/
+
+/* unique id sent to bootloader and required to put into a bootload state */
+#define UNIQUE_BL_ID 0x34
+
+/* mask for the flash data */
+#define FLASH_DATA_MASK 0x55
+
+/* bootloader commands */
+#define GET_SW_VERSION_COMMAND 0x00
+#define SET_PAGE_COMMAND 0x01
+#define ERASE_PAGE_COMMAND 0x02
+#define WRITE_PAGE_COMMAND 0x03
+#define CRC_ON_PAGE_COMMAND 0x04
+#define READ_FLASH_BYTE_COMMAND 0x05
+#define RESET_DEVICE_COMMAND 0x06
+#define GET_HW_VERSION_COMMAND 0x07
+#define BLANK 0xff
+
+/* bootloader command responses */
+#define COMMAND_OK 0x01
+#define COMMAND_FAILED 0x02
+#define COMMAND_PENDING 0x03
+
+
+
+/**************************************************************************
+ * General Driver Functions - REGISTER_REPORTs
+ **************************************************************************/
+
+/*
+ * si470x_get_report - receive a HID report
+ */
+static int si470x_get_report(struct si470x_device *radio, void *buf, int size)
+{
+ unsigned char *report = buf;
+ int retval;
+
+ retval = usb_control_msg(radio->usbdev,
+ usb_rcvctrlpipe(radio->usbdev, 0),
+ HID_REQ_GET_REPORT,
+ USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_IN,
+ report[0], 2,
+ buf, size, usb_timeout);
+
+ if (retval < 0)
+ dev_warn(&radio->intf->dev,
+ "si470x_get_report: usb_control_msg returned %d\n",
+ retval);
+ return retval;
+}
+
+
+/*
+ * si470x_set_report - send a HID report
+ */
+static int si470x_set_report(struct si470x_device *radio, void *buf, int size)
+{
+ unsigned char *report = buf;
+ int retval;
+
+ retval = usb_control_msg(radio->usbdev,
+ usb_sndctrlpipe(radio->usbdev, 0),
+ HID_REQ_SET_REPORT,
+ USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_OUT,
+ report[0], 2,
+ buf, size, usb_timeout);
+
+ if (retval < 0)
+ dev_warn(&radio->intf->dev,
+ "si470x_set_report: usb_control_msg returned %d\n",
+ retval);
+ return retval;
+}
+
+
+/*
+ * si470x_get_register - read register
+ */
+static int si470x_get_register(struct si470x_device *radio, int regnr)
+{
+ int retval;
+
+ radio->usb_buf[0] = REGISTER_REPORT(regnr);
+
+ retval = si470x_get_report(radio, radio->usb_buf, REGISTER_REPORT_SIZE);
+
+ if (retval >= 0)
+ radio->registers[regnr] = get_unaligned_be16(&radio->usb_buf[1]);
+
+ return (retval < 0) ? -EINVAL : 0;
+}
+
+
+/*
+ * si470x_set_register - write register
+ */
+static int si470x_set_register(struct si470x_device *radio, int regnr)
+{
+ int retval;
+
+ radio->usb_buf[0] = REGISTER_REPORT(regnr);
+ put_unaligned_be16(radio->registers[regnr], &radio->usb_buf[1]);
+
+ retval = si470x_set_report(radio, radio->usb_buf, REGISTER_REPORT_SIZE);
+
+ return (retval < 0) ? -EINVAL : 0;
+}
+
+
+
+/**************************************************************************
+ * General Driver Functions - ENTIRE_REPORT
+ **************************************************************************/
+
+/*
+ * si470x_get_all_registers - read entire registers
+ */
+static int si470x_get_all_registers(struct si470x_device *radio)
+{
+ int retval;
+ unsigned char regnr;
+
+ radio->usb_buf[0] = ENTIRE_REPORT;
+
+ retval = si470x_get_report(radio, radio->usb_buf, ENTIRE_REPORT_SIZE);
+
+ if (retval >= 0)
+ for (regnr = 0; regnr < RADIO_REGISTER_NUM; regnr++)
+ radio->registers[regnr] = get_unaligned_be16(
+ &radio->usb_buf[regnr * RADIO_REGISTER_SIZE + 1]);
+
+ return (retval < 0) ? -EINVAL : 0;
+}
+
+
+
+/**************************************************************************
+ * General Driver Functions - LED_REPORT
+ **************************************************************************/
+
+/*
+ * si470x_set_led_state - sets the led state
+ */
+static int si470x_set_led_state(struct si470x_device *radio,
+ unsigned char led_state)
+{
+ int retval;
+
+ radio->usb_buf[0] = LED_REPORT;
+ radio->usb_buf[1] = LED_COMMAND;
+ radio->usb_buf[2] = led_state;
+
+ retval = si470x_set_report(radio, radio->usb_buf, LED_REPORT_SIZE);
+
+ return (retval < 0) ? -EINVAL : 0;
+}
+
+
+
+/**************************************************************************
+ * General Driver Functions - SCRATCH_REPORT
+ **************************************************************************/
+
+/*
+ * si470x_get_scratch_versions - gets the scratch page and version infos
+ */
+static int si470x_get_scratch_page_versions(struct si470x_device *radio)
+{
+ int retval;
+
+ radio->usb_buf[0] = SCRATCH_REPORT;
+
+ retval = si470x_get_report(radio, radio->usb_buf, SCRATCH_REPORT_SIZE);
+
+ if (retval < 0)
+ dev_warn(&radio->intf->dev, "si470x_get_scratch: si470x_get_report returned %d\n",
+ retval);
+ else {
+ radio->software_version = radio->usb_buf[1];
+ radio->hardware_version = radio->usb_buf[2];
+ }
+
+ return (retval < 0) ? -EINVAL : 0;
+}
+
+
+
+/**************************************************************************
+ * RDS Driver Functions
+ **************************************************************************/
+
+/*
+ * si470x_int_in_callback - rds callback and processing function
+ *
+ * TODO: do we need to use mutex locks in some sections?
+ */
+static void si470x_int_in_callback(struct urb *urb)
+{
+ struct si470x_device *radio = urb->context;
+ int retval;
+ unsigned char regnr;
+ unsigned char blocknum;
+ unsigned short bler; /* rds block errors */
+ unsigned short rds;
+ unsigned char tmpbuf[3];
+
+ if (urb->status) {
+ if (urb->status == -ENOENT ||
+ urb->status == -ECONNRESET ||
+ urb->status == -ESHUTDOWN) {
+ return;
+ } else {
+ dev_warn(&radio->intf->dev,
+ "non-zero urb status (%d)\n", urb->status);
+ goto resubmit; /* Maybe we can recover. */
+ }
+ }
+
+ /* Sometimes the device returns len 0 packets */
+ if (urb->actual_length != RDS_REPORT_SIZE)
+ goto resubmit;
+
+ radio->registers[STATUSRSSI] =
+ get_unaligned_be16(&radio->int_in_buffer[1]);
+
+ if (radio->registers[STATUSRSSI] & STATUSRSSI_STC)
+ complete(&radio->completion);
+
+ if ((radio->registers[SYSCONFIG1] & SYSCONFIG1_RDS)) {
+ /* Update RDS registers with URB data */
+ for (regnr = 1; regnr < RDS_REGISTER_NUM; regnr++)
+ radio->registers[STATUSRSSI + regnr] =
+ get_unaligned_be16(&radio->int_in_buffer[
+ regnr * RADIO_REGISTER_SIZE + 1]);
+ /* get rds blocks */
+ if ((radio->registers[STATUSRSSI] & STATUSRSSI_RDSR) == 0) {
+ /* No RDS group ready, better luck next time */
+ goto resubmit;
+ }
+ if ((radio->registers[STATUSRSSI] & STATUSRSSI_RDSS) == 0) {
+ /* RDS decoder not synchronized */
+ goto resubmit;
+ }
+ for (blocknum = 0; blocknum < 4; blocknum++) {
+ switch (blocknum) {
+ default:
+ bler = (radio->registers[STATUSRSSI] &
+ STATUSRSSI_BLERA) >> 9;
+ rds = radio->registers[RDSA];
+ break;
+ case 1:
+ bler = (radio->registers[READCHAN] &
+ READCHAN_BLERB) >> 14;
+ rds = radio->registers[RDSB];
+ break;
+ case 2:
+ bler = (radio->registers[READCHAN] &
+ READCHAN_BLERC) >> 12;
+ rds = radio->registers[RDSC];
+ break;
+ case 3:
+ bler = (radio->registers[READCHAN] &
+ READCHAN_BLERD) >> 10;
+ rds = radio->registers[RDSD];
+ break;
+ }
+
+ /* Fill the V4L2 RDS buffer */
+ put_unaligned_le16(rds, &tmpbuf);
+ tmpbuf[2] = blocknum; /* offset name */
+ tmpbuf[2] |= blocknum << 3; /* received offset */
+ if (bler > max_rds_errors)
+ tmpbuf[2] |= 0x80; /* uncorrectable errors */
+ else if (bler > 0)
+ tmpbuf[2] |= 0x40; /* corrected error(s) */
+
+ /* copy RDS block to internal buffer */
+ memcpy(&radio->buffer[radio->wr_index], &tmpbuf, 3);
+ radio->wr_index += 3;
+
+ /* wrap write pointer */
+ if (radio->wr_index >= radio->buf_size)
+ radio->wr_index = 0;
+
+ /* check for overflow */
+ if (radio->wr_index == radio->rd_index) {
+ /* increment and wrap read pointer */
+ radio->rd_index += 3;
+ if (radio->rd_index >= radio->buf_size)
+ radio->rd_index = 0;
+ }
+ }
+ if (radio->wr_index != radio->rd_index)
+ wake_up_interruptible(&radio->read_queue);
+ }
+
+resubmit:
+ /* Resubmit if we're still running. */
+ if (radio->int_in_running && radio->usbdev) {
+ retval = usb_submit_urb(radio->int_in_urb, GFP_ATOMIC);
+ if (retval) {
+ dev_warn(&radio->intf->dev,
+ "resubmitting urb failed (%d)", retval);
+ radio->int_in_running = 0;
+ }
+ }
+ radio->status_rssi_auto_update = radio->int_in_running;
+}
+
+
+static int si470x_fops_open(struct file *file)
+{
+ return v4l2_fh_open(file);
+}
+
+static int si470x_fops_release(struct file *file)
+{
+ return v4l2_fh_release(file);
+}
+
+static void si470x_usb_release(struct v4l2_device *v4l2_dev)
+{
+ struct si470x_device *radio =
+ container_of(v4l2_dev, struct si470x_device, v4l2_dev);
+
+ usb_free_urb(radio->int_in_urb);
+ v4l2_ctrl_handler_free(&radio->hdl);
+ v4l2_device_unregister(&radio->v4l2_dev);
+ kfree(radio->int_in_buffer);
+ kfree(radio->buffer);
+ kfree(radio->usb_buf);
+ kfree(radio);
+}
+
+
+/**************************************************************************
+ * Video4Linux Interface
+ **************************************************************************/
+
+/*
+ * si470x_vidioc_querycap - query device capabilities
+ */
+static int si470x_vidioc_querycap(struct file *file, void *priv,
+ struct v4l2_capability *capability)
+{
+ struct si470x_device *radio = video_drvdata(file);
+
+ strscpy(capability->driver, DRIVER_NAME, sizeof(capability->driver));
+ strscpy(capability->card, DRIVER_CARD, sizeof(capability->card));
+ usb_make_path(radio->usbdev, capability->bus_info,
+ sizeof(capability->bus_info));
+ return 0;
+}
+
+
+static int si470x_start_usb(struct si470x_device *radio)
+{
+ int retval;
+
+ /* initialize interrupt urb */
+ usb_fill_int_urb(radio->int_in_urb, radio->usbdev,
+ usb_rcvintpipe(radio->usbdev,
+ radio->int_in_endpoint->bEndpointAddress),
+ radio->int_in_buffer,
+ le16_to_cpu(radio->int_in_endpoint->wMaxPacketSize),
+ si470x_int_in_callback,
+ radio,
+ radio->int_in_endpoint->bInterval);
+
+ radio->int_in_running = 1;
+ mb();
+
+ retval = usb_submit_urb(radio->int_in_urb, GFP_KERNEL);
+ if (retval) {
+ dev_info(&radio->intf->dev,
+ "submitting int urb failed (%d)\n", retval);
+ radio->int_in_running = 0;
+ }
+ radio->status_rssi_auto_update = radio->int_in_running;
+
+ /* start radio */
+ retval = si470x_start(radio);
+ if (retval < 0)
+ return retval;
+
+ v4l2_ctrl_handler_setup(&radio->hdl);
+
+ return retval;
+}
+
+/**************************************************************************
+ * USB Interface
+ **************************************************************************/
+
+/*
+ * si470x_usb_driver_probe - probe for the device
+ */
+static int si470x_usb_driver_probe(struct usb_interface *intf,
+ const struct usb_device_id *id)
+{
+ struct si470x_device *radio;
+ struct usb_host_interface *iface_desc;
+ int int_end_size, retval;
+ unsigned char version_warning = 0;
+
+ /* private data allocation and initialization */
+ radio = kzalloc_obj(struct si470x_device);
+ if (!radio) {
+ retval = -ENOMEM;
+ goto err_initial;
+ }
+ radio->usb_buf = kmalloc(MAX_REPORT_SIZE, GFP_KERNEL);
+ if (radio->usb_buf == NULL) {
+ retval = -ENOMEM;
+ goto err_radio;
+ }
+ radio->usbdev = interface_to_usbdev(intf);
+ radio->intf = intf;
+ radio->band = 1; /* Default to 76 - 108 MHz */
+ mutex_init(&radio->lock);
+ init_completion(&radio->completion);
+
+ radio->get_register = si470x_get_register;
+ radio->set_register = si470x_set_register;
+ radio->fops_open = si470x_fops_open;
+ radio->fops_release = si470x_fops_release;
+ radio->vidioc_querycap = si470x_vidioc_querycap;
+
+ iface_desc = intf->cur_altsetting;
+
+ /* Set up interrupt endpoint information. */
+ retval = usb_find_int_in_endpoint(iface_desc, &radio->int_in_endpoint);
+ if (retval) {
+ dev_info(&intf->dev, "could not find interrupt in endpoint\n");
+ retval = -EIO;
+ goto err_usbbuf;
+ }
+
+ int_end_size = le16_to_cpu(radio->int_in_endpoint->wMaxPacketSize);
+
+ radio->int_in_buffer = kmalloc(int_end_size, GFP_KERNEL);
+ if (!radio->int_in_buffer) {
+ dev_info(&intf->dev, "could not allocate int_in_buffer");
+ retval = -ENOMEM;
+ goto err_usbbuf;
+ }
+
+ radio->int_in_urb = usb_alloc_urb(0, GFP_KERNEL);
+ if (!radio->int_in_urb) {
+ retval = -ENOMEM;
+ goto err_intbuffer;
+ }
+
+ radio->v4l2_dev.release = si470x_usb_release;
+
+ /*
+ * The si470x SiLabs reference design uses the same USB IDs as
+ * 'Thanko's Raremono' si4734 based receiver. So check here which we
+ * have: attempt to read the device ID from the si470x: the lower 12
+ * bits should be 0x0242 for the si470x.
+ *
+ * We use this check to determine which device we are dealing with.
+ */
+ if (id->idVendor == 0x10c4 && id->idProduct == 0x818a) {
+ retval = usb_control_msg(radio->usbdev,
+ usb_rcvctrlpipe(radio->usbdev, 0),
+ HID_REQ_GET_REPORT,
+ USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_IN,
+ 1, 2,
+ radio->usb_buf, 3, 500);
+ if (retval != 3 ||
+ (get_unaligned_be16(&radio->usb_buf[1]) & 0xfff) != 0x0242) {
+ dev_info(&intf->dev, "this is not a si470x device.\n");
+ retval = -ENODEV;
+ goto err_urb;
+ }
+ }
+
+ retval = v4l2_device_register(&intf->dev, &radio->v4l2_dev);
+ if (retval < 0) {
+ dev_err(&intf->dev, "couldn't register v4l2_device\n");
+ goto err_urb;
+ }
+
+ v4l2_ctrl_handler_init(&radio->hdl, 2);
+ v4l2_ctrl_new_std(&radio->hdl, &si470x_ctrl_ops,
+ V4L2_CID_AUDIO_MUTE, 0, 1, 1, 1);
+ v4l2_ctrl_new_std(&radio->hdl, &si470x_ctrl_ops,
+ V4L2_CID_AUDIO_VOLUME, 0, 15, 1, 15);
+ if (radio->hdl.error) {
+ retval = radio->hdl.error;
+ dev_err(&intf->dev, "couldn't register control\n");
+ goto err_dev;
+ }
+ radio->videodev = si470x_viddev_template;
+ radio->videodev.ctrl_handler = &radio->hdl;
+ radio->videodev.lock = &radio->lock;
+ radio->videodev.v4l2_dev = &radio->v4l2_dev;
+ radio->videodev.release = video_device_release_empty;
+ radio->videodev.device_caps =
+ V4L2_CAP_HW_FREQ_SEEK | V4L2_CAP_READWRITE | V4L2_CAP_TUNER |
+ V4L2_CAP_RADIO | V4L2_CAP_RDS_CAPTURE;
+ video_set_drvdata(&radio->videodev, radio);
+
+ /* get device and chip versions */
+ if (si470x_get_all_registers(radio) < 0) {
+ retval = -EIO;
+ goto err_ctrl;
+ }
+ dev_info(&intf->dev, "DeviceID=0x%4.4hx ChipID=0x%4.4hx\n",
+ radio->registers[DEVICEID], radio->registers[SI_CHIPID]);
+ if ((radio->registers[SI_CHIPID] & SI_CHIPID_FIRMWARE) < RADIO_FW_VERSION) {
+ dev_warn(&intf->dev,
+ "This driver is known to work with firmware version %u, but the device has firmware version %u.\n",
+ RADIO_FW_VERSION,
+ radio->registers[SI_CHIPID] & SI_CHIPID_FIRMWARE);
+ version_warning = 1;
+ }
+
+ /* get software and hardware versions */
+ if (si470x_get_scratch_page_versions(radio) < 0) {
+ retval = -EIO;
+ goto err_ctrl;
+ }
+ dev_info(&intf->dev, "software version %d, hardware version %d\n",
+ radio->software_version, radio->hardware_version);
+ if (radio->hardware_version < RADIO_HW_VERSION) {
+ dev_warn(&intf->dev,
+ "This driver is known to work with hardware version %u, but the device has hardware version %u.\n",
+ RADIO_HW_VERSION,
+ radio->hardware_version);
+ version_warning = 1;
+ }
+
+ /* give out version warning */
+ if (version_warning == 1) {
+ dev_warn(&intf->dev,
+ "If you have some trouble using this driver, please report to V4L ML at linux-media@vger.kernel.org\n");
+ }
+
+ /* set led to connect state */
+ si470x_set_led_state(radio, BLINK_GREEN_LED);
+
+ /* rds buffer allocation */
+ radio->buf_size = rds_buf * 3;
+ radio->buffer = kmalloc(radio->buf_size, GFP_KERNEL);
+ if (!radio->buffer) {
+ retval = -EIO;
+ goto err_ctrl;
+ }
+
+ /* rds buffer configuration */
+ radio->wr_index = 0;
+ radio->rd_index = 0;
+ init_waitqueue_head(&radio->read_queue);
+ usb_set_intfdata(intf, radio);
+
+ /* start radio */
+ retval = si470x_start_usb(radio);
+ if (retval < 0 && !radio->int_in_running)
+ goto err_buf;
+ else if (retval < 0) /* in case of radio->int_in_running == 1 */
+ goto err_all;
+
+ /* set initial frequency */
+ si470x_set_freq(radio, 87.5 * FREQ_MUL); /* available in all regions */
+
+ /* register video device */
+ retval = video_register_device(&radio->videodev, VFL_TYPE_RADIO,
+ radio_nr);
+ if (retval) {
+ dev_err(&intf->dev, "Could not register video device\n");
+ goto err_all;
+ }
+
+ return 0;
+err_all:
+ usb_kill_urb(radio->int_in_urb);
+err_buf:
+ kfree(radio->buffer);
+err_ctrl:
+ v4l2_ctrl_handler_free(&radio->hdl);
+err_dev:
+ v4l2_device_unregister(&radio->v4l2_dev);
+err_urb:
+ usb_free_urb(radio->int_in_urb);
+err_intbuffer:
+ kfree(radio->int_in_buffer);
+err_usbbuf:
+ kfree(radio->usb_buf);
+err_radio:
+ kfree(radio);
+err_initial:
+ return retval;
+}
+
+
+/*
+ * si470x_usb_driver_suspend - suspend the device
+ */
+static int si470x_usb_driver_suspend(struct usb_interface *intf,
+ pm_message_t message)
+{
+ struct si470x_device *radio = usb_get_intfdata(intf);
+
+ dev_info(&intf->dev, "suspending now...\n");
+
+ /* shutdown interrupt handler */
+ if (radio->int_in_running) {
+ radio->int_in_running = 0;
+ if (radio->int_in_urb)
+ usb_kill_urb(radio->int_in_urb);
+ }
+
+ /* cancel read processes */
+ wake_up_interruptible(&radio->read_queue);
+
+ /* stop radio */
+ si470x_stop(radio);
+ return 0;
+}
+
+
+/*
+ * si470x_usb_driver_resume - resume the device
+ */
+static int si470x_usb_driver_resume(struct usb_interface *intf)
+{
+ struct si470x_device *radio = usb_get_intfdata(intf);
+ int ret;
+
+ dev_info(&intf->dev, "resuming now...\n");
+
+ /* start radio */
+ ret = si470x_start_usb(radio);
+ if (ret == 0)
+ v4l2_ctrl_handler_setup(&radio->hdl);
+
+ return ret;
+}
+
+
+/*
+ * si470x_usb_driver_disconnect - disconnect the device
+ */
+static void si470x_usb_driver_disconnect(struct usb_interface *intf)
+{
+ struct si470x_device *radio = usb_get_intfdata(intf);
+
+ mutex_lock(&radio->lock);
+ v4l2_device_disconnect(&radio->v4l2_dev);
+ video_unregister_device(&radio->videodev);
+ usb_kill_urb(radio->int_in_urb);
+ usb_set_intfdata(intf, NULL);
+ mutex_unlock(&radio->lock);
+ v4l2_device_put(&radio->v4l2_dev);
+}
+
+
+/*
+ * si470x_usb_driver - usb driver interface
+ *
+ * A note on suspend/resume: this driver had only empty suspend/resume
+ * functions, and when I tried to test suspend/resume it always disconnected
+ * instead of resuming (using my ADS InstantFM stick). So I've decided to
+ * remove these callbacks until someone else with better hardware can
+ * implement and test this.
+ */
+static struct usb_driver si470x_usb_driver = {
+ .name = DRIVER_NAME,
+ .probe = si470x_usb_driver_probe,
+ .disconnect = si470x_usb_driver_disconnect,
+ .suspend = si470x_usb_driver_suspend,
+ .resume = si470x_usb_driver_resume,
+ .reset_resume = si470x_usb_driver_resume,
+ .id_table = si470x_usb_driver_id_table,
+};
+
+module_usb_driver(si470x_usb_driver);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR(DRIVER_AUTHOR);
+MODULE_DESCRIPTION(DRIVER_DESC);
+MODULE_VERSION(DRIVER_VERSION);
diff --git a/drivers/media/radio/si470x/radio-si470x.h b/drivers/media/radio/si470x/radio-si470x.h
new file mode 100644
index 000000000..2915c0023
--- /dev/null
+++ b/drivers/media/radio/si470x/radio-si470x.h
@@ -0,0 +1,220 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * drivers/media/radio/si470x/radio-si470x.h
+ *
+ * Driver for radios with Silicon Labs Si470x FM Radio Receivers
+ *
+ * Copyright (c) 2009 Tobias Lorenz <tobias.lorenz@gmx.net>
+ */
+
+
+/* driver definitions */
+#define DRIVER_NAME "radio-si470x"
+
+
+/* kernel includes */
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/init.h>
+#include <linux/sched.h>
+#include <linux/slab.h>
+#include <linux/input.h>
+#include <linux/videodev2.h>
+#include <linux/mutex.h>
+#include <media/v4l2-common.h>
+#include <media/v4l2-ioctl.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-event.h>
+#include <media/v4l2-device.h>
+#include <linux/unaligned.h>
+
+
+
+/**************************************************************************
+ * Register Definitions
+ **************************************************************************/
+#define RADIO_REGISTER_SIZE 2 /* 16 register bit width */
+#define RADIO_REGISTER_NUM 16 /* DEVICEID ... RDSD */
+#define RDS_REGISTER_NUM 6 /* STATUSRSSI ... RDSD */
+
+#define DEVICEID 0 /* Device ID */
+#define DEVICEID_PN 0xf000 /* bits 15..12: Part Number */
+#define DEVICEID_MFGID 0x0fff /* bits 11..00: Manufacturer ID */
+
+#define SI_CHIPID 1 /* Chip ID */
+#define SI_CHIPID_REV 0xfc00 /* bits 15..10: Chip Version */
+#define SI_CHIPID_DEV 0x0200 /* bits 09..09: Device */
+#define SI_CHIPID_FIRMWARE 0x01ff /* bits 08..00: Firmware Version */
+
+#define POWERCFG 2 /* Power Configuration */
+#define POWERCFG_DSMUTE 0x8000 /* bits 15..15: Softmute Disable */
+#define POWERCFG_DMUTE 0x4000 /* bits 14..14: Mute Disable */
+#define POWERCFG_MONO 0x2000 /* bits 13..13: Mono Select */
+#define POWERCFG_RDSM 0x0800 /* bits 11..11: RDS Mode (Si4701 only) */
+#define POWERCFG_SKMODE 0x0400 /* bits 10..10: Seek Mode */
+#define POWERCFG_SEEKUP 0x0200 /* bits 09..09: Seek Direction */
+#define POWERCFG_SEEK 0x0100 /* bits 08..08: Seek */
+#define POWERCFG_DISABLE 0x0040 /* bits 06..06: Powerup Disable */
+#define POWERCFG_ENABLE 0x0001 /* bits 00..00: Powerup Enable */
+
+#define CHANNEL 3 /* Channel */
+#define CHANNEL_TUNE 0x8000 /* bits 15..15: Tune */
+#define CHANNEL_CHAN 0x03ff /* bits 09..00: Channel Select */
+
+#define SYSCONFIG1 4 /* System Configuration 1 */
+#define SYSCONFIG1_RDSIEN 0x8000 /* bits 15..15: RDS Interrupt Enable (Si4701 only) */
+#define SYSCONFIG1_STCIEN 0x4000 /* bits 14..14: Seek/Tune Complete Interrupt Enable */
+#define SYSCONFIG1_RDS 0x1000 /* bits 12..12: RDS Enable (Si4701 only) */
+#define SYSCONFIG1_DE 0x0800 /* bits 11..11: De-emphasis (0=75us 1=50us) */
+#define SYSCONFIG1_AGCD 0x0400 /* bits 10..10: AGC Disable */
+#define SYSCONFIG1_BLNDADJ 0x00c0 /* bits 07..06: Stereo/Mono Blend Level Adjustment */
+#define SYSCONFIG1_GPIO3 0x0030 /* bits 05..04: General Purpose I/O 3 */
+#define SYSCONFIG1_GPIO2 0x000c /* bits 03..02: General Purpose I/O 2 */
+#define SYSCONFIG1_GPIO2_DIS 0x0000 /* Disable GPIO 2 interrupt */
+#define SYSCONFIG1_GPIO2_INT 0x0004 /* Enable STC/RDS interrupt */
+#define SYSCONFIG1_GPIO1 0x0003 /* bits 01..00: General Purpose I/O 1 */
+
+#define SYSCONFIG2 5 /* System Configuration 2 */
+#define SYSCONFIG2_SEEKTH 0xff00 /* bits 15..08: RSSI Seek Threshold */
+#define SYSCONFIG2_BAND 0x00c0 /* bits 07..06: Band Select */
+#define SYSCONFIG2_SPACE 0x0030 /* bits 05..04: Channel Spacing */
+#define SYSCONFIG2_VOLUME 0x000f /* bits 03..00: Volume */
+
+#define SYSCONFIG3 6 /* System Configuration 3 */
+#define SYSCONFIG3_SMUTER 0xc000 /* bits 15..14: Softmute Attack/Recover Rate */
+#define SYSCONFIG3_SMUTEA 0x3000 /* bits 13..12: Softmute Attenuation */
+#define SYSCONFIG3_SKSNR 0x00f0 /* bits 07..04: Seek SNR Threshold */
+#define SYSCONFIG3_SKCNT 0x000f /* bits 03..00: Seek FM Impulse Detection Threshold */
+
+#define TEST1 7 /* Test 1 */
+#define TEST1_AHIZEN 0x4000 /* bits 14..14: Audio High-Z Enable */
+
+#define TEST2 8 /* Test 2 */
+/* TEST2 only contains reserved bits */
+
+#define BOOTCONFIG 9 /* Boot Configuration */
+/* BOOTCONFIG only contains reserved bits */
+
+#define STATUSRSSI 10 /* Status RSSI */
+#define STATUSRSSI_RDSR 0x8000 /* bits 15..15: RDS Ready (Si4701 only) */
+#define STATUSRSSI_STC 0x4000 /* bits 14..14: Seek/Tune Complete */
+#define STATUSRSSI_SF 0x2000 /* bits 13..13: Seek Fail/Band Limit */
+#define STATUSRSSI_AFCRL 0x1000 /* bits 12..12: AFC Rail */
+#define STATUSRSSI_RDSS 0x0800 /* bits 11..11: RDS Synchronized (Si4701 only) */
+#define STATUSRSSI_BLERA 0x0600 /* bits 10..09: RDS Block A Errors (Si4701 only) */
+#define STATUSRSSI_ST 0x0100 /* bits 08..08: Stereo Indicator */
+#define STATUSRSSI_RSSI 0x00ff /* bits 07..00: RSSI (Received Signal Strength Indicator) */
+
+#define READCHAN 11 /* Read Channel */
+#define READCHAN_BLERB 0xc000 /* bits 15..14: RDS Block D Errors (Si4701 only) */
+#define READCHAN_BLERC 0x3000 /* bits 13..12: RDS Block C Errors (Si4701 only) */
+#define READCHAN_BLERD 0x0c00 /* bits 11..10: RDS Block B Errors (Si4701 only) */
+#define READCHAN_READCHAN 0x03ff /* bits 09..00: Read Channel */
+
+#define RDSA 12 /* RDSA */
+#define RDSA_RDSA 0xffff /* bits 15..00: RDS Block A Data (Si4701 only) */
+
+#define RDSB 13 /* RDSB */
+#define RDSB_RDSB 0xffff /* bits 15..00: RDS Block B Data (Si4701 only) */
+
+#define RDSC 14 /* RDSC */
+#define RDSC_RDSC 0xffff /* bits 15..00: RDS Block C Data (Si4701 only) */
+
+#define RDSD 15 /* RDSD */
+#define RDSD_RDSD 0xffff /* bits 15..00: RDS Block D Data (Si4701 only) */
+
+
+
+/**************************************************************************
+ * General Driver Definitions
+ **************************************************************************/
+
+/*
+ * si470x_device - private data
+ */
+struct si470x_device {
+ struct v4l2_device v4l2_dev;
+ struct video_device videodev;
+ struct v4l2_ctrl_handler hdl;
+ int band;
+
+ /* Silabs internal registers (0..15) */
+ unsigned short registers[RADIO_REGISTER_NUM];
+
+ /* RDS receive buffer */
+ wait_queue_head_t read_queue;
+ struct mutex lock; /* buffer locking */
+ unsigned char *buffer; /* size is always multiple of three */
+ unsigned int buf_size;
+ unsigned int rd_index;
+ unsigned int wr_index;
+
+ struct completion completion;
+ bool status_rssi_auto_update; /* Does RSSI get updated automatic? */
+
+ /* si470x ops */
+
+ int (*get_register)(struct si470x_device *radio, int regnr);
+ int (*set_register)(struct si470x_device *radio, int regnr);
+ int (*fops_open)(struct file *file);
+ int (*fops_release)(struct file *file);
+ int (*vidioc_querycap)(struct file *file, void *priv,
+ struct v4l2_capability *capability);
+
+#if IS_ENABLED(CONFIG_USB_SI470X)
+ /* reference to USB and video device */
+ struct usb_device *usbdev;
+ struct usb_interface *intf;
+ char *usb_buf;
+
+ /* Interrupt endpoint handling */
+ char *int_in_buffer;
+ struct usb_endpoint_descriptor *int_in_endpoint;
+ struct urb *int_in_urb;
+ int int_in_running;
+
+ /* scratch page */
+ unsigned char software_version;
+ unsigned char hardware_version;
+#endif
+
+#if IS_ENABLED(CONFIG_I2C_SI470X)
+ struct i2c_client *client;
+ struct gpio_desc *gpio_reset;
+#endif
+};
+
+
+
+/**************************************************************************
+ * Firmware Versions
+ **************************************************************************/
+
+#define RADIO_FW_VERSION 12
+
+
+
+/**************************************************************************
+ * Frequency Multiplicator
+ **************************************************************************/
+
+/*
+ * The frequency is set in units of 62.5 Hz when using V4L2_TUNER_CAP_LOW,
+ * 62.5 kHz otherwise.
+ * The tuner is able to have a channel spacing of 50, 100 or 200 kHz.
+ * tuner->capability is therefore set to V4L2_TUNER_CAP_LOW
+ * The FREQ_MUL is then: 1 MHz / 62.5 Hz = 16000
+ */
+#define FREQ_MUL (1000000 / 62.5)
+
+
+
+/**************************************************************************
+ * Common Functions
+ **************************************************************************/
+extern const struct video_device si470x_viddev_template;
+extern const struct v4l2_ctrl_ops si470x_ctrl_ops;
+int si470x_disconnect_check(struct si470x_device *radio);
+int si470x_set_freq(struct si470x_device *radio, unsigned int freq);
+int si470x_start(struct si470x_device *radio);
+int si470x_stop(struct si470x_device *radio);
diff --git a/drivers/media/radio/si4713/Kconfig b/drivers/media/radio/si4713/Kconfig
new file mode 100644
index 000000000..490bc6f20
--- /dev/null
+++ b/drivers/media/radio/si4713/Kconfig
@@ -0,0 +1,41 @@
+# SPDX-License-Identifier: GPL-2.0-only
+config USB_SI4713
+ tristate "Silicon Labs Si4713 FM Radio Transmitter support with USB"
+ depends on USB && I2C && RADIO_SI4713
+ select I2C_SI4713
+ help
+ This is a driver for USB devices with the Silicon Labs SI4713
+ chip. Currently these devices are known to work.
+ - 10c4:8244: Silicon Labs FM Transmitter USB device.
+
+ Say Y here if you want to connect this type of radio to your
+ computer's USB port.
+
+ To compile this driver as a module, choose M here: the
+ module will be called radio-usb-si4713.
+
+config PLATFORM_SI4713
+ tristate "Silicon Labs Si4713 FM Radio Transmitter support with I2C"
+ depends on I2C && RADIO_SI4713
+ select I2C_SI4713
+ help
+ This is a driver for I2C devices with the Silicon Labs SI4713
+ chip.
+
+ Say Y here if you want to connect this type of radio to your
+ computer's I2C port.
+
+ To compile this driver as a module, choose M here: the
+ module will be called radio-platform-si4713.
+
+config I2C_SI4713
+ tristate "Silicon Labs Si4713 FM Radio Transmitter support"
+ depends on I2C && RADIO_SI4713
+ help
+ Say Y here if you want support to Si4713 FM Radio Transmitter.
+ This device can transmit audio through FM. It can transmit
+ RDS and RBDS signals as well. This module is the v4l2 radio
+ interface for the i2c driver of this device.
+
+ To compile this driver as a module, choose M here: the
+ module will be called si4713.
diff --git a/drivers/media/radio/si4713/Makefile b/drivers/media/radio/si4713/Makefile
new file mode 100644
index 000000000..3411514a5
--- /dev/null
+++ b/drivers/media/radio/si4713/Makefile
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Makefile for radios with Silicon Labs Si4713 FM Radio Transmitters
+#
+
+obj-$(CONFIG_I2C_SI4713) += si4713.o
+obj-$(CONFIG_USB_SI4713) += radio-usb-si4713.o
+obj-$(CONFIG_PLATFORM_SI4713) += radio-platform-si4713.o
diff --git a/drivers/media/radio/si4713/radio-platform-si4713.c b/drivers/media/radio/si4713/radio-platform-si4713.c
new file mode 100644
index 000000000..413296811
--- /dev/null
+++ b/drivers/media/radio/si4713/radio-platform-si4713.c
@@ -0,0 +1,211 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * drivers/media/radio/radio-si4713.c
+ *
+ * Platform Driver for Silicon Labs Si4713 FM Radio Transmitter:
+ *
+ * Copyright (c) 2008 Instituto Nokia de Tecnologia - INdT
+ * Contact: Eduardo Valentin <eduardo.valentin@nokia.com>
+ */
+
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/init.h>
+#include <linux/platform_device.h>
+#include <linux/i2c.h>
+#include <linux/videodev2.h>
+#include <linux/slab.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-common.h>
+#include <media/v4l2-ioctl.h>
+#include <media/v4l2-fh.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-event.h>
+#include "si4713.h"
+
+/* module parameters */
+static int radio_nr = -1; /* radio device minor (-1 ==> auto assign) */
+module_param(radio_nr, int, 0);
+MODULE_PARM_DESC(radio_nr,
+ "Minor number for radio device (-1 ==> auto assign)");
+
+MODULE_LICENSE("GPL v2");
+MODULE_AUTHOR("Eduardo Valentin <eduardo.valentin@nokia.com>");
+MODULE_DESCRIPTION("Platform driver for Si4713 FM Radio Transmitter");
+MODULE_VERSION("0.0.1");
+MODULE_ALIAS("platform:radio-si4713");
+
+/* Driver state struct */
+struct radio_si4713_device {
+ struct v4l2_device v4l2_dev;
+ struct video_device radio_dev;
+ struct mutex lock;
+};
+
+/* radio_si4713_fops - file operations interface */
+static const struct v4l2_file_operations radio_si4713_fops = {
+ .owner = THIS_MODULE,
+ .open = v4l2_fh_open,
+ .release = v4l2_fh_release,
+ .poll = v4l2_ctrl_poll,
+ /* Note: locking is done at the subdev level in the i2c driver. */
+ .unlocked_ioctl = video_ioctl2,
+};
+
+/* Video4Linux Interface */
+
+/* radio_si4713_querycap - query device capabilities */
+static int radio_si4713_querycap(struct file *file, void *priv,
+ struct v4l2_capability *capability)
+{
+ strscpy(capability->driver, "radio-si4713", sizeof(capability->driver));
+ strscpy(capability->card, "Silicon Labs Si4713 Modulator",
+ sizeof(capability->card));
+ strscpy(capability->bus_info, "platform:radio-si4713",
+ sizeof(capability->bus_info));
+ return 0;
+}
+
+/*
+ * v4l2 ioctl call backs.
+ * we are just a wrapper for v4l2_sub_devs.
+ */
+static inline struct v4l2_device *get_v4l2_dev(struct file *file)
+{
+ return &((struct radio_si4713_device *)video_drvdata(file))->v4l2_dev;
+}
+
+static int radio_si4713_g_modulator(struct file *file, void *priv,
+ struct v4l2_modulator *vm)
+{
+ return v4l2_device_call_until_err(get_v4l2_dev(file), 0, tuner,
+ g_modulator, vm);
+}
+
+static int radio_si4713_s_modulator(struct file *file, void *priv,
+ const struct v4l2_modulator *vm)
+{
+ return v4l2_device_call_until_err(get_v4l2_dev(file), 0, tuner,
+ s_modulator, vm);
+}
+
+static int radio_si4713_g_frequency(struct file *file, void *priv,
+ struct v4l2_frequency *vf)
+{
+ return v4l2_device_call_until_err(get_v4l2_dev(file), 0, tuner,
+ g_frequency, vf);
+}
+
+static int radio_si4713_s_frequency(struct file *file, void *priv,
+ const struct v4l2_frequency *vf)
+{
+ return v4l2_device_call_until_err(get_v4l2_dev(file), 0, tuner,
+ s_frequency, vf);
+}
+
+static long radio_si4713_default(struct file *file, void *priv,
+ bool valid_prio, unsigned int cmd, void *arg)
+{
+ return v4l2_device_call_until_err(get_v4l2_dev(file), 0, core,
+ ioctl, cmd, arg);
+}
+
+static const struct v4l2_ioctl_ops radio_si4713_ioctl_ops = {
+ .vidioc_querycap = radio_si4713_querycap,
+ .vidioc_g_modulator = radio_si4713_g_modulator,
+ .vidioc_s_modulator = radio_si4713_s_modulator,
+ .vidioc_g_frequency = radio_si4713_g_frequency,
+ .vidioc_s_frequency = radio_si4713_s_frequency,
+ .vidioc_log_status = v4l2_ctrl_log_status,
+ .vidioc_subscribe_event = v4l2_ctrl_subscribe_event,
+ .vidioc_unsubscribe_event = v4l2_event_unsubscribe,
+ .vidioc_default = radio_si4713_default,
+};
+
+/* radio_si4713_vdev_template - video device interface */
+static const struct video_device radio_si4713_vdev_template = {
+ .fops = &radio_si4713_fops,
+ .name = "radio-si4713",
+ .release = video_device_release_empty,
+ .ioctl_ops = &radio_si4713_ioctl_ops,
+ .vfl_dir = VFL_DIR_TX,
+};
+
+/* Platform driver interface */
+/* radio_si4713_pdriver_probe - probe for the device */
+static int radio_si4713_pdriver_probe(struct platform_device *pdev)
+{
+ struct radio_si4713_platform_data *pdata = pdev->dev.platform_data;
+ struct radio_si4713_device *rsdev;
+ struct v4l2_subdev *sd;
+ int rval = 0;
+
+ if (!pdata) {
+ dev_err(&pdev->dev, "Cannot proceed without platform data.\n");
+ rval = -EINVAL;
+ goto exit;
+ }
+
+ rsdev = devm_kzalloc(&pdev->dev, sizeof(*rsdev), GFP_KERNEL);
+ if (!rsdev) {
+ dev_err(&pdev->dev, "Failed to alloc video device.\n");
+ rval = -ENOMEM;
+ goto exit;
+ }
+ mutex_init(&rsdev->lock);
+
+ rval = v4l2_device_register(&pdev->dev, &rsdev->v4l2_dev);
+ if (rval) {
+ dev_err(&pdev->dev, "Failed to register v4l2 device.\n");
+ goto exit;
+ }
+
+ sd = i2c_get_clientdata(pdata->subdev);
+ rval = v4l2_device_register_subdev(&rsdev->v4l2_dev, sd);
+ if (rval) {
+ dev_err(&pdev->dev, "Cannot get v4l2 subdevice\n");
+ goto unregister_v4l2_dev;
+ }
+
+ rsdev->radio_dev = radio_si4713_vdev_template;
+ rsdev->radio_dev.v4l2_dev = &rsdev->v4l2_dev;
+ rsdev->radio_dev.ctrl_handler = sd->ctrl_handler;
+ /* Serialize all access to the si4713 */
+ rsdev->radio_dev.lock = &rsdev->lock;
+ rsdev->radio_dev.device_caps = V4L2_CAP_MODULATOR | V4L2_CAP_RDS_OUTPUT;
+ video_set_drvdata(&rsdev->radio_dev, rsdev);
+ if (video_register_device(&rsdev->radio_dev, VFL_TYPE_RADIO, radio_nr)) {
+ dev_err(&pdev->dev, "Could not register video device.\n");
+ rval = -EIO;
+ goto unregister_v4l2_dev;
+ }
+ dev_info(&pdev->dev, "New device successfully probed\n");
+
+ goto exit;
+
+unregister_v4l2_dev:
+ v4l2_device_unregister(&rsdev->v4l2_dev);
+exit:
+ return rval;
+}
+
+/* radio_si4713_pdriver_remove - remove the device */
+static void radio_si4713_pdriver_remove(struct platform_device *pdev)
+{
+ struct v4l2_device *v4l2_dev = platform_get_drvdata(pdev);
+ struct radio_si4713_device *rsdev;
+
+ rsdev = container_of(v4l2_dev, struct radio_si4713_device, v4l2_dev);
+ video_unregister_device(&rsdev->radio_dev);
+ v4l2_device_unregister(&rsdev->v4l2_dev);
+}
+
+static struct platform_driver radio_si4713_pdriver = {
+ .driver = {
+ .name = "radio-si4713",
+ },
+ .probe = radio_si4713_pdriver_probe,
+ .remove = radio_si4713_pdriver_remove,
+};
+
+module_platform_driver(radio_si4713_pdriver);
diff --git a/drivers/media/radio/si4713/radio-usb-si4713.c b/drivers/media/radio/si4713/radio-usb-si4713.c
new file mode 100644
index 000000000..6e6764143
--- /dev/null
+++ b/drivers/media/radio/si4713/radio-usb-si4713.c
@@ -0,0 +1,522 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright 2013 Cisco Systems, Inc. and/or its affiliates.
+ * All rights reserved.
+ */
+
+/* kernel includes */
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/usb.h>
+#include <linux/init.h>
+#include <linux/slab.h>
+#include <linux/input.h>
+#include <linux/mutex.h>
+#include <linux/i2c.h>
+/* V4l includes */
+#include <linux/videodev2.h>
+#include <media/v4l2-common.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-ioctl.h>
+#include <media/v4l2-event.h>
+#include <linux/platform_data/media/si4713.h>
+
+#include "si4713.h"
+
+/* driver and module definitions */
+MODULE_AUTHOR("Dinesh Ram <dinesh.ram@cern.ch>");
+MODULE_DESCRIPTION("Si4713 FM Transmitter USB driver");
+MODULE_LICENSE("GPL v2");
+
+/* The Device announces itself as Cygnal Integrated Products, Inc. */
+#define USB_SI4713_VENDOR 0x10c4
+#define USB_SI4713_PRODUCT 0x8244
+
+#define BUFFER_LENGTH 64
+#define USB_TIMEOUT 1000
+#define USB_RESP_TIMEOUT 50000
+
+/* USB Device ID List */
+static const struct usb_device_id usb_si4713_usb_device_table[] = {
+ {USB_DEVICE_AND_INTERFACE_INFO(USB_SI4713_VENDOR, USB_SI4713_PRODUCT,
+ USB_CLASS_HID, 0, 0) },
+ { } /* Terminating entry */
+};
+
+MODULE_DEVICE_TABLE(usb, usb_si4713_usb_device_table);
+
+struct si4713_usb_device {
+ struct usb_device *usbdev;
+ struct usb_interface *intf;
+ struct video_device vdev;
+ struct v4l2_device v4l2_dev;
+ struct v4l2_subdev *v4l2_subdev;
+ struct mutex lock;
+ struct i2c_adapter i2c_adapter;
+
+ u8 *buffer;
+};
+
+static inline struct si4713_usb_device *to_si4713_dev(struct v4l2_device *v4l2_dev)
+{
+ return container_of(v4l2_dev, struct si4713_usb_device, v4l2_dev);
+}
+
+static int vidioc_querycap(struct file *file, void *priv,
+ struct v4l2_capability *v)
+{
+ struct si4713_usb_device *radio = video_drvdata(file);
+
+ strscpy(v->driver, "radio-usb-si4713", sizeof(v->driver));
+ strscpy(v->card, "Si4713 FM Transmitter", sizeof(v->card));
+ usb_make_path(radio->usbdev, v->bus_info, sizeof(v->bus_info));
+ return 0;
+}
+
+static int vidioc_g_modulator(struct file *file, void *priv,
+ struct v4l2_modulator *vm)
+{
+ struct si4713_usb_device *radio = video_drvdata(file);
+
+ return v4l2_subdev_call(radio->v4l2_subdev, tuner, g_modulator, vm);
+}
+
+static int vidioc_s_modulator(struct file *file, void *priv,
+ const struct v4l2_modulator *vm)
+{
+ struct si4713_usb_device *radio = video_drvdata(file);
+
+ return v4l2_subdev_call(radio->v4l2_subdev, tuner, s_modulator, vm);
+}
+
+static int vidioc_s_frequency(struct file *file, void *priv,
+ const struct v4l2_frequency *vf)
+{
+ struct si4713_usb_device *radio = video_drvdata(file);
+
+ return v4l2_subdev_call(radio->v4l2_subdev, tuner, s_frequency, vf);
+}
+
+static int vidioc_g_frequency(struct file *file, void *priv,
+ struct v4l2_frequency *vf)
+{
+ struct si4713_usb_device *radio = video_drvdata(file);
+
+ return v4l2_subdev_call(radio->v4l2_subdev, tuner, g_frequency, vf);
+}
+
+static const struct v4l2_ioctl_ops usb_si4713_ioctl_ops = {
+ .vidioc_querycap = vidioc_querycap,
+ .vidioc_g_modulator = vidioc_g_modulator,
+ .vidioc_s_modulator = vidioc_s_modulator,
+ .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,
+};
+
+/* File system interface */
+static const struct v4l2_file_operations usb_si4713_fops = {
+ .owner = THIS_MODULE,
+ .open = v4l2_fh_open,
+ .release = v4l2_fh_release,
+ .poll = v4l2_ctrl_poll,
+ .unlocked_ioctl = video_ioctl2,
+};
+
+static void usb_si4713_video_device_release(struct v4l2_device *v4l2_dev)
+{
+ struct si4713_usb_device *radio = to_si4713_dev(v4l2_dev);
+ struct i2c_adapter *adapter = &radio->i2c_adapter;
+
+ i2c_del_adapter(adapter);
+ v4l2_device_unregister(&radio->v4l2_dev);
+ kfree(radio->buffer);
+ kfree(radio);
+}
+
+/*
+ * This command sequence emulates the behaviour of the Windows driver.
+ * The structure of these commands was determined by sniffing the
+ * usb traffic of the device during startup.
+ * Most likely, these commands make some queries to the device.
+ * Commands are sent to enquire parameters like the bus mode,
+ * component revision, boot mode, the device serial number etc.
+ *
+ * These commands are necessary to be sent in this order during startup.
+ * The device fails to powerup if these commands are not sent.
+ *
+ * The complete list of startup commands is given in the start_seq table below.
+ */
+static int si4713_send_startup_command(struct si4713_usb_device *radio)
+{
+ unsigned long until_jiffies = jiffies + usecs_to_jiffies(USB_RESP_TIMEOUT) + 1;
+ u8 *buffer = radio->buffer;
+ int retval;
+
+ /* send the command */
+ retval = usb_control_msg(radio->usbdev, usb_sndctrlpipe(radio->usbdev, 0),
+ 0x09, 0x21, 0x033f, 0, radio->buffer,
+ BUFFER_LENGTH, USB_TIMEOUT);
+ if (retval < 0)
+ return retval;
+
+ for (;;) {
+ /* receive the response */
+ retval = usb_control_msg(radio->usbdev, usb_rcvctrlpipe(radio->usbdev, 0),
+ 0x01, 0xa1, 0x033f, 0, radio->buffer,
+ BUFFER_LENGTH, USB_TIMEOUT);
+ if (retval < 0)
+ return retval;
+ if (!radio->buffer[1]) {
+ /* USB traffic sniffing showed that some commands require
+ * additional checks. */
+ switch (buffer[1]) {
+ case 0x32:
+ if (radio->buffer[2] == 0)
+ return 0;
+ break;
+ case 0x14:
+ case 0x12:
+ if (radio->buffer[2] & SI4713_CTS)
+ return 0;
+ break;
+ case 0x06:
+ if ((radio->buffer[2] & SI4713_CTS) && radio->buffer[9] == 0x08)
+ return 0;
+ break;
+ default:
+ return 0;
+ }
+ }
+ if (time_is_before_jiffies(until_jiffies))
+ return -EIO;
+ msleep(3);
+ }
+
+ return retval;
+}
+
+struct si4713_start_seq_table {
+ int len;
+ u8 payload[8];
+};
+
+/*
+ * Some of the startup commands that could be recognized are :
+ * (0x03): Get serial number of the board (Response : CB000-00-00)
+ * (0x06, 0x03, 0x03, 0x08, 0x01, 0x0f) : Get Component revision
+ */
+static const struct si4713_start_seq_table start_seq[] = {
+
+ { 1, { 0x03 } },
+ { 2, { 0x32, 0x7f } },
+ { 6, { 0x06, 0x03, 0x03, 0x08, 0x01, 0x0f } },
+ { 2, { 0x14, 0x02 } },
+ { 2, { 0x09, 0x90 } },
+ { 3, { 0x08, 0x90, 0xfa } },
+ { 2, { 0x36, 0x01 } },
+ { 2, { 0x05, 0x03 } },
+ { 7, { 0x06, 0x00, 0x06, 0x0e, 0x01, 0x0f, 0x05 } },
+ { 1, { 0x12 } },
+ /* Commands that are sent after pressing the 'Initialize'
+ button in the windows application */
+ { 1, { 0x03 } },
+ { 1, { 0x01 } },
+ { 2, { 0x09, 0x90 } },
+ { 3, { 0x08, 0x90, 0xfa } },
+ { 1, { 0x34 } },
+ { 2, { 0x35, 0x01 } },
+ { 2, { 0x36, 0x01 } },
+ { 2, { 0x30, 0x09 } },
+ { 4, { 0x30, 0x06, 0x00, 0xe2 } },
+ { 3, { 0x31, 0x01, 0x30 } },
+ { 3, { 0x31, 0x04, 0x09 } },
+ { 2, { 0x05, 0x02 } },
+ { 6, { 0x06, 0x03, 0x03, 0x08, 0x01, 0x0f } },
+};
+
+static int si4713_start_seq(struct si4713_usb_device *radio)
+{
+ int retval = 0;
+ int i;
+
+ radio->buffer[0] = 0x3f;
+
+ for (i = 0; i < ARRAY_SIZE(start_seq); i++) {
+ int len = start_seq[i].len;
+ const u8 *payload = start_seq[i].payload;
+
+ memcpy(radio->buffer + 1, payload, len);
+ memset(radio->buffer + len + 1, 0, BUFFER_LENGTH - 1 - len);
+ retval = si4713_send_startup_command(radio);
+ }
+
+ return retval;
+}
+
+static struct i2c_board_info si4713_board_info = {
+ I2C_BOARD_INFO("si4713", SI4713_I2C_ADDR_BUSEN_HIGH),
+};
+
+struct si4713_command_table {
+ int command_id;
+ u8 payload[8];
+};
+
+/*
+ * Structure of a command :
+ * Byte 1 : 0x3f (always)
+ * Byte 2 : 0x06 (send a command)
+ * Byte 3 : Unknown
+ * Byte 4 : Number of arguments + 1 (for the command byte)
+ * Byte 5 : Number of response bytes
+ */
+static struct si4713_command_table command_table[] = {
+
+ { SI4713_CMD_POWER_UP, { 0x00, SI4713_PWUP_NARGS + 1, SI4713_PWUP_NRESP} },
+ { SI4713_CMD_GET_REV, { 0x03, 0x01, SI4713_GETREV_NRESP } },
+ { SI4713_CMD_POWER_DOWN, { 0x00, 0x01, SI4713_PWDN_NRESP} },
+ { SI4713_CMD_SET_PROPERTY, { 0x00, SI4713_SET_PROP_NARGS + 1, SI4713_SET_PROP_NRESP } },
+ { SI4713_CMD_GET_PROPERTY, { 0x00, SI4713_GET_PROP_NARGS + 1, SI4713_GET_PROP_NRESP } },
+ { SI4713_CMD_TX_TUNE_FREQ, { 0x03, SI4713_TXFREQ_NARGS + 1, SI4713_TXFREQ_NRESP } },
+ { SI4713_CMD_TX_TUNE_POWER, { 0x03, SI4713_TXPWR_NARGS + 1, SI4713_TXPWR_NRESP } },
+ { SI4713_CMD_TX_TUNE_MEASURE, { 0x03, SI4713_TXMEA_NARGS + 1, SI4713_TXMEA_NRESP } },
+ { SI4713_CMD_TX_TUNE_STATUS, { 0x00, SI4713_TXSTATUS_NARGS + 1, SI4713_TXSTATUS_NRESP } },
+ { SI4713_CMD_TX_ASQ_STATUS, { 0x03, SI4713_ASQSTATUS_NARGS + 1, SI4713_ASQSTATUS_NRESP } },
+ { SI4713_CMD_GET_INT_STATUS, { 0x03, 0x01, SI4713_GET_STATUS_NRESP } },
+ { SI4713_CMD_TX_RDS_BUFF, { 0x03, SI4713_RDSBUFF_NARGS + 1, SI4713_RDSBUFF_NRESP } },
+ { SI4713_CMD_TX_RDS_PS, { 0x00, SI4713_RDSPS_NARGS + 1, SI4713_RDSPS_NRESP } },
+};
+
+static int send_command(struct si4713_usb_device *radio, u8 *payload, char *data, int len)
+{
+ int retval;
+
+ radio->buffer[0] = 0x3f;
+ radio->buffer[1] = 0x06;
+
+ memcpy(radio->buffer + 2, payload, 3);
+ memcpy(radio->buffer + 5, data, len);
+ memset(radio->buffer + 5 + len, 0, BUFFER_LENGTH - 5 - len);
+
+ /* send the command */
+ retval = usb_control_msg(radio->usbdev, usb_sndctrlpipe(radio->usbdev, 0),
+ 0x09, 0x21, 0x033f, 0, radio->buffer,
+ BUFFER_LENGTH, USB_TIMEOUT);
+
+ return retval < 0 ? retval : 0;
+}
+
+static int si4713_i2c_read(struct si4713_usb_device *radio, char *data, int len)
+{
+ unsigned long until_jiffies = jiffies + usecs_to_jiffies(USB_RESP_TIMEOUT) + 1;
+ int retval;
+
+ /* receive the response */
+ for (;;) {
+ retval = usb_control_msg(radio->usbdev,
+ usb_rcvctrlpipe(radio->usbdev, 0),
+ 0x01, 0xa1, 0x033f, 0, radio->buffer,
+ BUFFER_LENGTH, USB_TIMEOUT);
+ if (retval < 0)
+ return retval;
+
+ /*
+ * Check that we get a valid reply back (buffer[1] == 0) and
+ * that CTS is set before returning, otherwise we wait and try
+ * again. The i2c driver also does the CTS check, but the timeouts
+ * used there are much too small for this USB driver, so we wait
+ * for it here.
+ */
+ if (radio->buffer[1] == 0 && (radio->buffer[2] & SI4713_CTS)) {
+ memcpy(data, radio->buffer + 2, len);
+ return 0;
+ }
+ if (time_is_before_jiffies(until_jiffies)) {
+ /* Zero the status value, ensuring CTS isn't set */
+ data[0] = 0;
+ return 0;
+ }
+ msleep(3);
+ }
+}
+
+static int si4713_i2c_write(struct si4713_usb_device *radio, char *data, int len)
+{
+ int retval = -EINVAL;
+ int i;
+
+ if (len > BUFFER_LENGTH - 5)
+ return -EINVAL;
+
+ for (i = 0; i < ARRAY_SIZE(command_table); i++) {
+ if (data[0] == command_table[i].command_id)
+ retval = send_command(radio, command_table[i].payload,
+ data, len);
+ }
+
+ return retval < 0 ? retval : 0;
+}
+
+static int si4713_transfer(struct i2c_adapter *i2c_adapter,
+ struct i2c_msg *msgs, int num)
+{
+ struct si4713_usb_device *radio = i2c_get_adapdata(i2c_adapter);
+ int retval = -EINVAL;
+ int i;
+
+ for (i = 0; i < num; i++) {
+ if (msgs[i].flags & I2C_M_RD)
+ retval = si4713_i2c_read(radio, msgs[i].buf, msgs[i].len);
+ else
+ retval = si4713_i2c_write(radio, msgs[i].buf, msgs[i].len);
+ if (retval)
+ break;
+ }
+
+ return retval ? retval : num;
+}
+
+static u32 si4713_functionality(struct i2c_adapter *adapter)
+{
+ return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL;
+}
+
+static const struct i2c_algorithm si4713_algo = {
+ .master_xfer = si4713_transfer,
+ .functionality = si4713_functionality,
+};
+
+/* This name value shows up in the sysfs filename associated
+ with this I2C adapter */
+static const struct i2c_adapter si4713_i2c_adapter_template = {
+ .name = "si4713-i2c",
+ .owner = THIS_MODULE,
+ .algo = &si4713_algo,
+};
+
+static int si4713_register_i2c_adapter(struct si4713_usb_device *radio)
+{
+ radio->i2c_adapter = si4713_i2c_adapter_template;
+ /* set up sysfs linkage to our parent device */
+ radio->i2c_adapter.dev.parent = &radio->usbdev->dev;
+ i2c_set_adapdata(&radio->i2c_adapter, radio);
+
+ return i2c_add_adapter(&radio->i2c_adapter);
+}
+
+/* check if the device is present and register with v4l and usb if it is */
+static int usb_si4713_probe(struct usb_interface *intf,
+ const struct usb_device_id *id)
+{
+ struct si4713_usb_device *radio;
+ struct i2c_adapter *adapter;
+ struct v4l2_subdev *sd;
+ int retval;
+
+ dev_info(&intf->dev, "Si4713 development board discovered: (%04X:%04X)\n",
+ id->idVendor, id->idProduct);
+
+ /* Initialize local device structure */
+ radio = kzalloc_obj(struct si4713_usb_device);
+ if (radio)
+ radio->buffer = kmalloc(BUFFER_LENGTH, GFP_KERNEL);
+
+ if (!radio || !radio->buffer) {
+ dev_err(&intf->dev, "kmalloc for si4713_usb_device failed\n");
+ kfree(radio);
+ return -ENOMEM;
+ }
+
+ mutex_init(&radio->lock);
+
+ radio->usbdev = interface_to_usbdev(intf);
+ radio->intf = intf;
+ usb_set_intfdata(intf, &radio->v4l2_dev);
+
+ retval = si4713_start_seq(radio);
+ if (retval < 0)
+ goto err_v4l2;
+
+ 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;
+ }
+
+ retval = si4713_register_i2c_adapter(radio);
+ if (retval < 0) {
+ dev_err(&intf->dev, "could not register i2c device\n");
+ goto err_i2cdev;
+ }
+
+ adapter = &radio->i2c_adapter;
+ sd = v4l2_i2c_new_subdev_board(&radio->v4l2_dev, adapter,
+ &si4713_board_info, NULL);
+ radio->v4l2_subdev = sd;
+ if (!sd) {
+ dev_err(&intf->dev, "cannot get v4l2 subdevice\n");
+ retval = -ENODEV;
+ goto del_adapter;
+ }
+
+ radio->vdev.ctrl_handler = sd->ctrl_handler;
+ radio->v4l2_dev.release = usb_si4713_video_device_release;
+ strscpy(radio->vdev.name, radio->v4l2_dev.name,
+ sizeof(radio->vdev.name));
+ radio->vdev.v4l2_dev = &radio->v4l2_dev;
+ radio->vdev.fops = &usb_si4713_fops;
+ radio->vdev.ioctl_ops = &usb_si4713_ioctl_ops;
+ radio->vdev.lock = &radio->lock;
+ radio->vdev.release = video_device_release_empty;
+ radio->vdev.vfl_dir = VFL_DIR_TX;
+ radio->vdev.device_caps = V4L2_CAP_MODULATOR | V4L2_CAP_RDS_OUTPUT;
+
+ video_set_drvdata(&radio->vdev, radio);
+
+ retval = video_register_device(&radio->vdev, VFL_TYPE_RADIO, -1);
+ if (retval < 0) {
+ dev_err(&intf->dev, "could not register video device\n");
+ goto del_adapter;
+ }
+
+ dev_info(&intf->dev, "V4L2 device registered as %s\n",
+ video_device_node_name(&radio->vdev));
+
+ return 0;
+
+del_adapter:
+ i2c_del_adapter(adapter);
+err_i2cdev:
+ v4l2_device_unregister(&radio->v4l2_dev);
+err_v4l2:
+ kfree(radio->buffer);
+ kfree(radio);
+ return retval;
+}
+
+static void usb_si4713_disconnect(struct usb_interface *intf)
+{
+ struct si4713_usb_device *radio = to_si4713_dev(usb_get_intfdata(intf));
+
+ dev_info(&intf->dev, "Si4713 development board now disconnected\n");
+
+ mutex_lock(&radio->lock);
+ usb_set_intfdata(intf, NULL);
+ video_unregister_device(&radio->vdev);
+ v4l2_device_disconnect(&radio->v4l2_dev);
+ mutex_unlock(&radio->lock);
+ v4l2_device_put(&radio->v4l2_dev);
+}
+
+/* USB subsystem interface */
+static struct usb_driver usb_si4713_driver = {
+ .name = "radio-usb-si4713",
+ .probe = usb_si4713_probe,
+ .disconnect = usb_si4713_disconnect,
+ .id_table = usb_si4713_usb_device_table,
+};
+
+module_usb_driver(usb_si4713_driver);
diff --git a/drivers/media/radio/si4713/si4713.c b/drivers/media/radio/si4713/si4713.c
new file mode 100644
index 000000000..0c0354566
--- /dev/null
+++ b/drivers/media/radio/si4713/si4713.c
@@ -0,0 +1,1665 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * drivers/media/radio/si4713-i2c.c
+ *
+ * Silicon Labs Si4713 FM Radio Transmitter I2C commands.
+ *
+ * Copyright (c) 2009 Nokia Corporation
+ * Contact: Eduardo Valentin <eduardo.valentin@nokia.com>
+ */
+
+#include <linux/completion.h>
+#include <linux/delay.h>
+#include <linux/err.h>
+#include <linux/interrupt.h>
+#include <linux/i2c.h>
+#include <linux/slab.h>
+#include <linux/gpio/consumer.h>
+#include <linux/module.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-ioctl.h>
+#include <media/v4l2-common.h>
+
+#include "si4713.h"
+
+/* module parameters */
+static int debug;
+module_param(debug, int, S_IRUGO | S_IWUSR);
+MODULE_PARM_DESC(debug, "Debug level (0 - 2)");
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Eduardo Valentin <eduardo.valentin@nokia.com>");
+MODULE_DESCRIPTION("I2C driver for Si4713 FM Radio Transmitter");
+MODULE_VERSION("0.0.1");
+
+#define DEFAULT_RDS_PI 0x00
+#define DEFAULT_RDS_PTY 0x00
+#define DEFAULT_RDS_DEVIATION 0x00C8
+#define DEFAULT_RDS_PS_REPEAT_COUNT 0x0003
+#define DEFAULT_LIMITER_RTIME 0x1392
+#define DEFAULT_LIMITER_DEV 0x102CA
+#define DEFAULT_PILOT_FREQUENCY 0x4A38
+#define DEFAULT_PILOT_DEVIATION 0x1A5E
+#define DEFAULT_ACOMP_ATIME 0x0000
+#define DEFAULT_ACOMP_RTIME 0xF4240L
+#define DEFAULT_ACOMP_GAIN 0x0F
+#define DEFAULT_ACOMP_THRESHOLD (-0x28)
+#define DEFAULT_MUTE 0x01
+#define DEFAULT_POWER_LEVEL 88
+#define DEFAULT_FREQUENCY 8800
+#define DEFAULT_PREEMPHASIS FMPE_EU
+#define DEFAULT_TUNE_RNL 0xFF
+
+#define to_si4713_device(sd) container_of(sd, struct si4713_device, sd)
+
+/* frequency domain transformation (using times 10 to avoid floats) */
+#define FREQDEV_UNIT 100000
+#define FREQV4L2_MULTI 625
+#define si4713_to_v4l2(f) ((f * FREQDEV_UNIT) / FREQV4L2_MULTI)
+#define v4l2_to_si4713(f) ((f * FREQV4L2_MULTI) / FREQDEV_UNIT)
+#define FREQ_RANGE_LOW 7600
+#define FREQ_RANGE_HIGH 10800
+
+#define MAX_ARGS 7
+
+#define RDS_BLOCK 8
+#define RDS_BLOCK_CLEAR 0x03
+#define RDS_BLOCK_LOAD 0x04
+#define RDS_RADIOTEXT_2A 0x20
+#define RDS_RADIOTEXT_BLK_SIZE 4
+#define RDS_RADIOTEXT_INDEX_MAX 0x0F
+#define RDS_CARRIAGE_RETURN 0x0D
+
+#define rds_ps_nblocks(len) ((len / RDS_BLOCK) + (len % RDS_BLOCK ? 1 : 0))
+
+#define get_status_bit(p, b, m) (((p) & (m)) >> (b))
+#define set_bits(p, v, b, m) (((p) & ~(m)) | ((v) << (b)))
+
+#define ATTACK_TIME_UNIT 500
+
+#define POWER_OFF 0x00
+#define POWER_ON 0x01
+
+#define msb(x) ((u8)((u16) x >> 8))
+#define lsb(x) ((u8)((u16) x & 0x00FF))
+#define compose_u16(msb, lsb) (((u16)msb << 8) | lsb)
+#define check_command_failed(status) (!(status & SI4713_CTS) || \
+ (status & SI4713_ERR))
+/* mute definition */
+#define set_mute(p) (((p) & 1) | (((p) & 1) << 1))
+
+#ifdef DEBUG
+#define DBG_BUFFER(device, message, buffer, size) \
+ { \
+ int i; \
+ char str[(size)*5]; \
+ for (i = 0; i < size; i++) \
+ sprintf(str + i * 5, " 0x%02x", buffer[i]); \
+ v4l2_dbg(2, debug, device, "%s:%s\n", message, str); \
+ }
+#else
+#define DBG_BUFFER(device, message, buffer, size)
+#endif
+
+/*
+ * Values for limiter release time (sorted by second column)
+ * device release
+ * value time (us)
+ */
+static long limiter_times[] = {
+ 2000, 250,
+ 1000, 500,
+ 510, 1000,
+ 255, 2000,
+ 170, 3000,
+ 127, 4020,
+ 102, 5010,
+ 85, 6020,
+ 73, 7010,
+ 64, 7990,
+ 57, 8970,
+ 51, 10030,
+ 25, 20470,
+ 17, 30110,
+ 13, 39380,
+ 10, 51190,
+ 8, 63690,
+ 7, 73140,
+ 6, 85330,
+ 5, 102390,
+};
+
+/*
+ * Values for audio compression release time (sorted by second column)
+ * device release
+ * value time (us)
+ */
+static unsigned long acomp_rtimes[] = {
+ 0, 100000,
+ 1, 200000,
+ 2, 350000,
+ 3, 525000,
+ 4, 1000000,
+};
+
+/*
+ * Values for preemphasis (sorted by second column)
+ * device preemphasis
+ * value value (v4l2)
+ */
+static unsigned long preemphasis_values[] = {
+ FMPE_DISABLED, V4L2_PREEMPHASIS_DISABLED,
+ FMPE_EU, V4L2_PREEMPHASIS_50_uS,
+ FMPE_USA, V4L2_PREEMPHASIS_75_uS,
+};
+
+static int usecs_to_dev(unsigned long usecs, unsigned long const array[],
+ int size)
+{
+ int i;
+ int rval = -EINVAL;
+
+ for (i = 0; i < size / 2; i++)
+ if (array[(i * 2) + 1] >= usecs) {
+ rval = array[i * 2];
+ break;
+ }
+
+ return rval;
+}
+
+/* si4713_handler: IRQ handler, just complete work */
+static irqreturn_t si4713_handler(int irq, void *dev)
+{
+ struct si4713_device *sdev = dev;
+
+ v4l2_dbg(2, debug, &sdev->sd,
+ "%s: sending signal to completion work.\n", __func__);
+ complete(&sdev->work);
+
+ return IRQ_HANDLED;
+}
+
+/*
+ * si4713_send_command - sends a command to si4713 and waits its response
+ * @sdev: si4713_device structure for the device we are communicating
+ * @command: command id
+ * @args: command arguments we are sending (up to 7)
+ * @argn: actual size of @args
+ * @response: buffer to place the expected response from the device (up to 15)
+ * @respn: actual size of @response
+ * @usecs: amount of time to wait before reading the response (in usecs)
+ */
+static int si4713_send_command(struct si4713_device *sdev, const u8 command,
+ const u8 args[], const int argn,
+ u8 response[], const int respn, const int usecs)
+{
+ struct i2c_client *client = v4l2_get_subdevdata(&sdev->sd);
+ unsigned long until_jiffies;
+ u8 data1[MAX_ARGS + 1];
+ int err;
+
+ if (!client->adapter)
+ return -ENODEV;
+
+ /* First send the command and its arguments */
+ data1[0] = command;
+ memcpy(data1 + 1, args, argn);
+ DBG_BUFFER(&sdev->sd, "Parameters", data1, argn + 1);
+
+ err = i2c_master_send(client, data1, argn + 1);
+ if (err != argn + 1) {
+ v4l2_err(&sdev->sd, "Error while sending command 0x%02x\n",
+ command);
+ return err < 0 ? err : -EIO;
+ }
+
+ until_jiffies = jiffies + usecs_to_jiffies(usecs) + 1;
+
+ /* Wait response from interrupt */
+ if (client->irq) {
+ if (!wait_for_completion_timeout(&sdev->work,
+ usecs_to_jiffies(usecs) + 1))
+ v4l2_warn(&sdev->sd,
+ "(%s) Device took too much time to answer.\n",
+ __func__);
+ }
+
+ do {
+ err = i2c_master_recv(client, response, respn);
+ if (err != respn) {
+ v4l2_err(&sdev->sd,
+ "Error %d while reading response for command 0x%02x\n",
+ err, command);
+ return err < 0 ? err : -EIO;
+ }
+
+ DBG_BUFFER(&sdev->sd, "Response", response, respn);
+ if (!check_command_failed(response[0]))
+ return 0;
+
+ if (client->irq)
+ return -EBUSY;
+ if (usecs <= 1000)
+ usleep_range(usecs, 1000);
+ else
+ usleep_range(1000, 2000);
+ } while (time_is_after_jiffies(until_jiffies));
+
+ return -EBUSY;
+}
+
+/*
+ * si4713_read_property - reads a si4713 property
+ * @sdev: si4713_device structure for the device we are communicating
+ * @prop: property identification number
+ * @pv: property value to be returned on success
+ */
+static int si4713_read_property(struct si4713_device *sdev, u16 prop, u32 *pv)
+{
+ int err;
+ u8 val[SI4713_GET_PROP_NRESP];
+ /*
+ * .First byte = 0
+ * .Second byte = property's MSB
+ * .Third byte = property's LSB
+ */
+ const u8 args[SI4713_GET_PROP_NARGS] = {
+ 0x00,
+ msb(prop),
+ lsb(prop),
+ };
+
+ err = si4713_send_command(sdev, SI4713_CMD_GET_PROPERTY,
+ args, ARRAY_SIZE(args), val,
+ ARRAY_SIZE(val), DEFAULT_TIMEOUT);
+
+ if (err < 0)
+ return err;
+
+ *pv = compose_u16(val[2], val[3]);
+
+ v4l2_dbg(1, debug, &sdev->sd,
+ "%s: property=0x%02x value=0x%02x status=0x%02x\n",
+ __func__, prop, *pv, val[0]);
+
+ return err;
+}
+
+/*
+ * si4713_write_property - modifies a si4713 property
+ * @sdev: si4713_device structure for the device we are communicating
+ * @prop: property identification number
+ * @val: new value for that property
+ */
+static int si4713_write_property(struct si4713_device *sdev, u16 prop, u16 val)
+{
+ int rval;
+ u8 resp[SI4713_SET_PROP_NRESP];
+ /*
+ * .First byte = 0
+ * .Second byte = property's MSB
+ * .Third byte = property's LSB
+ * .Fourth byte = value's MSB
+ * .Fifth byte = value's LSB
+ */
+ const u8 args[SI4713_SET_PROP_NARGS] = {
+ 0x00,
+ msb(prop),
+ lsb(prop),
+ msb(val),
+ lsb(val),
+ };
+
+ rval = si4713_send_command(sdev, SI4713_CMD_SET_PROPERTY,
+ args, ARRAY_SIZE(args),
+ resp, ARRAY_SIZE(resp),
+ DEFAULT_TIMEOUT);
+
+ if (rval < 0)
+ return rval;
+
+ v4l2_dbg(1, debug, &sdev->sd,
+ "%s: property=0x%02x value=0x%02x status=0x%02x\n",
+ __func__, prop, val, resp[0]);
+
+ /*
+ * As there is no command response for SET_PROPERTY,
+ * wait Tcomp time to finish before proceed, in order
+ * to have property properly set.
+ */
+ msleep(TIMEOUT_SET_PROPERTY);
+
+ return rval;
+}
+
+/*
+ * si4713_powerup - Powers the device up
+ * @sdev: si4713_device structure for the device we are communicating
+ */
+static int si4713_powerup(struct si4713_device *sdev)
+{
+ struct i2c_client *client = v4l2_get_subdevdata(&sdev->sd);
+ int err;
+ u8 resp[SI4713_PWUP_NRESP];
+ /*
+ * .First byte = Enabled interrupts and boot function
+ * .Second byte = Input operation mode
+ */
+ u8 args[SI4713_PWUP_NARGS] = {
+ SI4713_PWUP_GPO2OEN | SI4713_PWUP_FUNC_TX,
+ SI4713_PWUP_OPMOD_ANALOG,
+ };
+
+ if (sdev->power_state)
+ return 0;
+
+ if (sdev->vdd) {
+ err = regulator_enable(sdev->vdd);
+ if (err) {
+ v4l2_err(&sdev->sd, "Failed to enable vdd: %d\n", err);
+ return err;
+ }
+ }
+
+ if (sdev->vio) {
+ err = regulator_enable(sdev->vio);
+ if (err) {
+ v4l2_err(&sdev->sd, "Failed to enable vio: %d\n", err);
+ return err;
+ }
+ }
+
+ if (sdev->gpio_reset) {
+ udelay(50);
+ gpiod_set_value(sdev->gpio_reset, 1);
+ }
+
+ if (client->irq)
+ args[0] |= SI4713_PWUP_CTSIEN;
+
+ err = si4713_send_command(sdev, SI4713_CMD_POWER_UP,
+ args, ARRAY_SIZE(args),
+ resp, ARRAY_SIZE(resp),
+ TIMEOUT_POWER_UP);
+
+ if (!err) {
+ v4l2_dbg(1, debug, &sdev->sd, "Powerup response: 0x%02x\n",
+ resp[0]);
+ v4l2_dbg(1, debug, &sdev->sd, "Device in power up mode\n");
+ sdev->power_state = POWER_ON;
+
+ if (client->irq)
+ err = si4713_write_property(sdev, SI4713_GPO_IEN,
+ SI4713_STC_INT | SI4713_CTS);
+ return err;
+ }
+ gpiod_set_value(sdev->gpio_reset, 0);
+
+
+ if (sdev->vdd) {
+ err = regulator_disable(sdev->vdd);
+ if (err)
+ v4l2_err(&sdev->sd, "Failed to disable vdd: %d\n", err);
+ }
+
+ if (sdev->vio) {
+ err = regulator_disable(sdev->vio);
+ if (err)
+ v4l2_err(&sdev->sd, "Failed to disable vio: %d\n", err);
+ }
+
+ return err;
+}
+
+/*
+ * si4713_powerdown - Powers the device down
+ * @sdev: si4713_device structure for the device we are communicating
+ */
+static int si4713_powerdown(struct si4713_device *sdev)
+{
+ int err;
+ u8 resp[SI4713_PWDN_NRESP];
+
+ if (!sdev->power_state)
+ return 0;
+
+ err = si4713_send_command(sdev, SI4713_CMD_POWER_DOWN,
+ NULL, 0,
+ resp, ARRAY_SIZE(resp),
+ DEFAULT_TIMEOUT);
+
+ if (!err) {
+ v4l2_dbg(1, debug, &sdev->sd, "Power down response: 0x%02x\n",
+ resp[0]);
+ v4l2_dbg(1, debug, &sdev->sd, "Device in reset mode\n");
+ if (sdev->gpio_reset)
+ gpiod_set_value(sdev->gpio_reset, 0);
+
+ if (sdev->vdd) {
+ err = regulator_disable(sdev->vdd);
+ if (err) {
+ v4l2_err(&sdev->sd,
+ "Failed to disable vdd: %d\n", err);
+ }
+ }
+
+ if (sdev->vio) {
+ err = regulator_disable(sdev->vio);
+ if (err) {
+ v4l2_err(&sdev->sd,
+ "Failed to disable vio: %d\n", err);
+ }
+ }
+ sdev->power_state = POWER_OFF;
+ }
+
+ return err;
+}
+
+/*
+ * si4713_checkrev - Checks if we are treating a device with the correct rev.
+ * @sdev: si4713_device structure for the device we are communicating
+ */
+static int si4713_checkrev(struct si4713_device *sdev)
+{
+ struct i2c_client *client = v4l2_get_subdevdata(&sdev->sd);
+ int rval;
+ u8 resp[SI4713_GETREV_NRESP];
+
+ rval = si4713_send_command(sdev, SI4713_CMD_GET_REV,
+ NULL, 0,
+ resp, ARRAY_SIZE(resp),
+ DEFAULT_TIMEOUT);
+
+ if (rval < 0)
+ return rval;
+
+ if (resp[1] == SI4713_PRODUCT_NUMBER) {
+ v4l2_info(&sdev->sd, "chip found @ 0x%02x (%s)\n",
+ client->addr << 1, client->adapter->name);
+ } else {
+ v4l2_err(&sdev->sd, "Invalid product number 0x%X\n", resp[1]);
+ rval = -EINVAL;
+ }
+ return rval;
+}
+
+/*
+ * si4713_wait_stc - Waits STC interrupt and clears status bits. Useful
+ * for TX_TUNE_POWER, TX_TUNE_FREQ and TX_TUNE_MEAS
+ * @sdev: si4713_device structure for the device we are communicating
+ * @usecs: timeout to wait for STC interrupt signal
+ */
+static int si4713_wait_stc(struct si4713_device *sdev, const int usecs)
+{
+ struct i2c_client *client = v4l2_get_subdevdata(&sdev->sd);
+ u8 resp[SI4713_GET_STATUS_NRESP];
+ unsigned long start_jiffies = jiffies;
+ int err;
+
+ if (client->irq &&
+ !wait_for_completion_timeout(&sdev->work, usecs_to_jiffies(usecs) + 1))
+ v4l2_warn(&sdev->sd,
+ "(%s) Device took too much time to answer.\n", __func__);
+
+ for (;;) {
+ /* Clear status bits */
+ err = si4713_send_command(sdev, SI4713_CMD_GET_INT_STATUS,
+ NULL, 0,
+ resp, ARRAY_SIZE(resp),
+ DEFAULT_TIMEOUT);
+ /* The USB device returns errors when it waits for the
+ * STC bit to be set. Hence polling */
+ if (err >= 0) {
+ v4l2_dbg(1, debug, &sdev->sd,
+ "%s: status bits: 0x%02x\n", __func__, resp[0]);
+
+ if (resp[0] & SI4713_STC_INT)
+ return 0;
+ }
+ if (jiffies_to_usecs(jiffies - start_jiffies) > usecs)
+ return err < 0 ? err : -EIO;
+ /* We sleep here for 3-4 ms in order to avoid flooding the device
+ * with USB requests. The si4713 USB driver was developed
+ * by reverse engineering the Windows USB driver. The windows
+ * driver also has a ~2.5 ms delay between responses. */
+ usleep_range(3000, 4000);
+ }
+}
+
+/*
+ * si4713_tx_tune_freq - Sets the state of the RF carrier and sets the tuning
+ * frequency between 76 and 108 MHz in 10 kHz units and
+ * steps of 50 kHz.
+ * @sdev: si4713_device structure for the device we are communicating
+ * @frequency: desired frequency (76 - 108 MHz, unit 10 KHz, step 50 kHz)
+ */
+static int si4713_tx_tune_freq(struct si4713_device *sdev, u16 frequency)
+{
+ int err;
+ u8 val[SI4713_TXFREQ_NRESP];
+ /*
+ * .First byte = 0
+ * .Second byte = frequency's MSB
+ * .Third byte = frequency's LSB
+ */
+ const u8 args[SI4713_TXFREQ_NARGS] = {
+ 0x00,
+ msb(frequency),
+ lsb(frequency),
+ };
+
+ err = si4713_send_command(sdev, SI4713_CMD_TX_TUNE_FREQ,
+ args, ARRAY_SIZE(args), val,
+ ARRAY_SIZE(val), DEFAULT_TIMEOUT);
+
+ if (err < 0)
+ return err;
+
+ v4l2_dbg(1, debug, &sdev->sd,
+ "%s: frequency=0x%02x status=0x%02x\n", __func__,
+ frequency, val[0]);
+
+ err = si4713_wait_stc(sdev, TIMEOUT_TX_TUNE);
+ if (err < 0)
+ return err;
+
+ return compose_u16(args[1], args[2]);
+}
+
+/*
+ * si4713_tx_tune_power - Sets the RF voltage level between 88 and 120 dBuV in
+ * 1 dB units. A value of 0x00 indicates off. The command
+ * also sets the antenna tuning capacitance. A value of 0
+ * indicates autotuning, and a value of 1 - 191 indicates
+ * a manual override, which results in a tuning
+ * capacitance of 0.25 pF x @antcap.
+ * @sdev: si4713_device structure for the device we are communicating
+ * @power: tuning power (88 - 120 dBuV, unit/step 1 dB)
+ * @antcap: value of antenna tuning capacitor (0 - 191)
+ */
+static int si4713_tx_tune_power(struct si4713_device *sdev, u8 power,
+ u8 antcap)
+{
+ int err;
+ u8 val[SI4713_TXPWR_NRESP];
+ /*
+ * .First byte = 0
+ * .Second byte = 0
+ * .Third byte = power
+ * .Fourth byte = antcap
+ */
+ u8 args[SI4713_TXPWR_NARGS] = {
+ 0x00,
+ 0x00,
+ power,
+ antcap,
+ };
+
+ /* Map power values 1-87 to MIN_POWER (88) */
+ if (power > 0 && power < SI4713_MIN_POWER)
+ args[2] = power = SI4713_MIN_POWER;
+
+ err = si4713_send_command(sdev, SI4713_CMD_TX_TUNE_POWER,
+ args, ARRAY_SIZE(args), val,
+ ARRAY_SIZE(val), DEFAULT_TIMEOUT);
+
+ if (err < 0)
+ return err;
+
+ v4l2_dbg(1, debug, &sdev->sd,
+ "%s: power=0x%02x antcap=0x%02x status=0x%02x\n",
+ __func__, power, antcap, val[0]);
+
+ return si4713_wait_stc(sdev, TIMEOUT_TX_TUNE_POWER);
+}
+
+/*
+ * si4713_tx_tune_measure - Enters receive mode and measures the received noise
+ * level in units of dBuV on the selected frequency.
+ * The Frequency must be between 76 and 108 MHz in 10 kHz
+ * units and steps of 50 kHz. The command also sets the
+ * antenna tuning capacitance. A value of 0 means
+ * autotuning, and a value of 1 to 191 indicates manual
+ * override.
+ * @sdev: si4713_device structure for the device we are communicating
+ * @frequency: desired frequency (76 - 108 MHz, unit 10 KHz, step 50 kHz)
+ * @antcap: value of antenna tuning capacitor (0 - 191)
+ */
+static int si4713_tx_tune_measure(struct si4713_device *sdev, u16 frequency,
+ u8 antcap)
+{
+ int err;
+ u8 val[SI4713_TXMEA_NRESP];
+ /*
+ * .First byte = 0
+ * .Second byte = frequency's MSB
+ * .Third byte = frequency's LSB
+ * .Fourth byte = antcap
+ */
+ const u8 args[SI4713_TXMEA_NARGS] = {
+ 0x00,
+ msb(frequency),
+ lsb(frequency),
+ antcap,
+ };
+
+ sdev->tune_rnl = DEFAULT_TUNE_RNL;
+
+ if (antcap > SI4713_MAX_ANTCAP)
+ return -EDOM;
+
+ err = si4713_send_command(sdev, SI4713_CMD_TX_TUNE_MEASURE,
+ args, ARRAY_SIZE(args), val,
+ ARRAY_SIZE(val), DEFAULT_TIMEOUT);
+
+ if (err < 0)
+ return err;
+
+ v4l2_dbg(1, debug, &sdev->sd,
+ "%s: frequency=0x%02x antcap=0x%02x status=0x%02x\n",
+ __func__, frequency, antcap, val[0]);
+
+ return si4713_wait_stc(sdev, TIMEOUT_TX_TUNE);
+}
+
+/*
+ * si4713_tx_tune_status- Returns the status of the tx_tune_freq, tx_tune_mea or
+ * tx_tune_power commands. This command return the current
+ * frequency, output voltage in dBuV, the antenna tunning
+ * capacitance value and the received noise level. The
+ * command also clears the stcint interrupt bit when the
+ * first bit of its arguments is high.
+ * @sdev: si4713_device structure for the device we are communicating
+ * @intack: 0x01 to clear the seek/tune complete interrupt status indicator.
+ * @frequency: returned frequency
+ * @power: returned power
+ * @antcap: returned antenna capacitance
+ * @noise: returned noise level
+ */
+static int si4713_tx_tune_status(struct si4713_device *sdev, u8 intack,
+ u16 *frequency, u8 *power,
+ u8 *antcap, u8 *noise)
+{
+ int err;
+ u8 val[SI4713_TXSTATUS_NRESP];
+ /*
+ * .First byte = intack bit
+ */
+ const u8 args[SI4713_TXSTATUS_NARGS] = {
+ intack & SI4713_INTACK_MASK,
+ };
+
+ err = si4713_send_command(sdev, SI4713_CMD_TX_TUNE_STATUS,
+ args, ARRAY_SIZE(args), val,
+ ARRAY_SIZE(val), DEFAULT_TIMEOUT);
+
+ if (!err) {
+ v4l2_dbg(1, debug, &sdev->sd,
+ "%s: status=0x%02x\n", __func__, val[0]);
+ *frequency = compose_u16(val[2], val[3]);
+ sdev->frequency = *frequency;
+ *power = val[5];
+ *antcap = val[6];
+ *noise = val[7];
+ v4l2_dbg(1, debug, &sdev->sd,
+ "%s: response: %d x 10 kHz (power %d, antcap %d, rnl %d)\n",
+ __func__, *frequency, *power, *antcap, *noise);
+ }
+
+ return err;
+}
+
+/*
+ * si4713_tx_rds_buff - Loads the RDS group buffer FIFO or circular buffer.
+ * @sdev: si4713_device structure for the device we are communicating
+ * @mode: the buffer operation mode.
+ * @rdsb: RDS Block B
+ * @rdsc: RDS Block C
+ * @rdsd: RDS Block D
+ * @cbleft: returns the number of available circular buffer blocks minus the
+ * number of used circular buffer blocks.
+ */
+static int si4713_tx_rds_buff(struct si4713_device *sdev, u8 mode, u16 rdsb,
+ u16 rdsc, u16 rdsd, s8 *cbleft)
+{
+ int err;
+ u8 val[SI4713_RDSBUFF_NRESP];
+
+ const u8 args[SI4713_RDSBUFF_NARGS] = {
+ mode & SI4713_RDSBUFF_MODE_MASK,
+ msb(rdsb),
+ lsb(rdsb),
+ msb(rdsc),
+ lsb(rdsc),
+ msb(rdsd),
+ lsb(rdsd),
+ };
+
+ err = si4713_send_command(sdev, SI4713_CMD_TX_RDS_BUFF,
+ args, ARRAY_SIZE(args), val,
+ ARRAY_SIZE(val), DEFAULT_TIMEOUT);
+
+ if (!err) {
+ v4l2_dbg(1, debug, &sdev->sd,
+ "%s: status=0x%02x\n", __func__, val[0]);
+ *cbleft = (s8)val[2] - val[3];
+ v4l2_dbg(1, debug, &sdev->sd,
+ "%s: response: interrupts 0x%02x cb avail: %d cb used %d fifo avail %d fifo used %d\n",
+ __func__, val[1], val[2], val[3], val[4], val[5]);
+ }
+
+ return err;
+}
+
+/*
+ * si4713_tx_rds_ps - Loads the program service buffer.
+ * @sdev: si4713_device structure for the device we are communicating
+ * @psid: program service id to be loaded.
+ * @pschar: assumed 4 size char array to be loaded into the program service
+ */
+static int si4713_tx_rds_ps(struct si4713_device *sdev, u8 psid,
+ unsigned char *pschar)
+{
+ int err;
+ u8 val[SI4713_RDSPS_NRESP];
+
+ const u8 args[SI4713_RDSPS_NARGS] = {
+ psid & SI4713_RDSPS_PSID_MASK,
+ pschar[0],
+ pschar[1],
+ pschar[2],
+ pschar[3],
+ };
+
+ err = si4713_send_command(sdev, SI4713_CMD_TX_RDS_PS,
+ args, ARRAY_SIZE(args), val,
+ ARRAY_SIZE(val), DEFAULT_TIMEOUT);
+
+ if (err < 0)
+ return err;
+
+ v4l2_dbg(1, debug, &sdev->sd, "%s: status=0x%02x\n", __func__, val[0]);
+
+ return err;
+}
+
+static int si4713_set_power_state(struct si4713_device *sdev, u8 value)
+{
+ if (value)
+ return si4713_powerup(sdev);
+ return si4713_powerdown(sdev);
+}
+
+static int si4713_set_mute(struct si4713_device *sdev, u16 mute)
+{
+ int rval = 0;
+
+ mute = set_mute(mute);
+
+ if (sdev->power_state)
+ rval = si4713_write_property(sdev,
+ SI4713_TX_LINE_INPUT_MUTE, mute);
+
+ return rval;
+}
+
+static int si4713_set_rds_ps_name(struct si4713_device *sdev, char *ps_name)
+{
+ int rval = 0, i;
+ u8 len = 0;
+
+ /* We want to clear the whole thing */
+ if (!strlen(ps_name))
+ memset(ps_name, 0, MAX_RDS_PS_NAME + 1);
+
+ if (sdev->power_state) {
+ /* Write the new ps name and clear the padding */
+ for (i = 0; i < MAX_RDS_PS_NAME; i += (RDS_BLOCK / 2)) {
+ rval = si4713_tx_rds_ps(sdev, (i / (RDS_BLOCK / 2)),
+ ps_name + i);
+ if (rval < 0)
+ return rval;
+ }
+
+ /* Setup the size to be sent */
+ if (strlen(ps_name))
+ len = strlen(ps_name) - 1;
+ else
+ len = 1;
+
+ rval = si4713_write_property(sdev,
+ SI4713_TX_RDS_PS_MESSAGE_COUNT,
+ rds_ps_nblocks(len));
+ if (rval < 0)
+ return rval;
+
+ rval = si4713_write_property(sdev,
+ SI4713_TX_RDS_PS_REPEAT_COUNT,
+ DEFAULT_RDS_PS_REPEAT_COUNT * 2);
+ if (rval < 0)
+ return rval;
+ }
+
+ return rval;
+}
+
+static int si4713_set_rds_radio_text(struct si4713_device *sdev, const char *rt)
+{
+ static const char cr[RDS_RADIOTEXT_BLK_SIZE] = { RDS_CARRIAGE_RETURN, 0 };
+ int rval = 0, i;
+ u16 t_index = 0;
+ u8 b_index = 0, cr_inserted = 0;
+ s8 left;
+
+ if (!sdev->power_state)
+ return rval;
+
+ rval = si4713_tx_rds_buff(sdev, RDS_BLOCK_CLEAR, 0, 0, 0, &left);
+ if (rval < 0)
+ return rval;
+
+ if (!strlen(rt))
+ return rval;
+
+ do {
+ /* RDS spec says that if the last block isn't used,
+ * then apply a carriage return
+ */
+ if (t_index < (RDS_RADIOTEXT_INDEX_MAX * RDS_RADIOTEXT_BLK_SIZE)) {
+ for (i = 0; i < RDS_RADIOTEXT_BLK_SIZE; i++) {
+ if (!rt[t_index + i] ||
+ rt[t_index + i] == RDS_CARRIAGE_RETURN) {
+ rt = cr;
+ cr_inserted = 1;
+ break;
+ }
+ }
+ }
+
+ rval = si4713_tx_rds_buff(sdev, RDS_BLOCK_LOAD,
+ compose_u16(RDS_RADIOTEXT_2A, b_index++),
+ compose_u16(rt[t_index], rt[t_index + 1]),
+ compose_u16(rt[t_index + 2], rt[t_index + 3]),
+ &left);
+ if (rval < 0)
+ return rval;
+
+ t_index += RDS_RADIOTEXT_BLK_SIZE;
+
+ if (cr_inserted)
+ break;
+ } while (left > 0);
+
+ return rval;
+}
+
+/*
+ * si4713_update_tune_status - update properties from tx_tune_status
+ * command. Must be called with sdev->mutex held.
+ * @sdev: si4713_device structure for the device we are communicating
+ */
+static int si4713_update_tune_status(struct si4713_device *sdev)
+{
+ int rval;
+ u16 f = 0;
+ u8 p = 0, a = 0, n = 0;
+
+ rval = si4713_tx_tune_status(sdev, 0x00, &f, &p, &a, &n);
+
+ if (rval < 0)
+ goto exit;
+
+/* TODO: check that power_level and antenna_capacitor really are not
+ changed by the hardware. If they are, then these controls should become
+ volatiles.
+ sdev->power_level = p;
+ sdev->antenna_capacitor = a;*/
+ sdev->tune_rnl = n;
+
+exit:
+ return rval;
+}
+
+static int si4713_choose_econtrol_action(struct si4713_device *sdev, u32 id,
+ s32 *bit, s32 *mask, u16 *property, int *mul,
+ unsigned long **table, int *size)
+{
+ s32 rval = 0;
+
+ switch (id) {
+ /* FM_TX class controls */
+ case V4L2_CID_RDS_TX_PI:
+ *property = SI4713_TX_RDS_PI;
+ *mul = 1;
+ break;
+ case V4L2_CID_AUDIO_COMPRESSION_THRESHOLD:
+ *property = SI4713_TX_ACOMP_THRESHOLD;
+ *mul = 1;
+ break;
+ case V4L2_CID_AUDIO_COMPRESSION_GAIN:
+ *property = SI4713_TX_ACOMP_GAIN;
+ *mul = 1;
+ break;
+ case V4L2_CID_PILOT_TONE_FREQUENCY:
+ *property = SI4713_TX_PILOT_FREQUENCY;
+ *mul = 1;
+ break;
+ case V4L2_CID_AUDIO_COMPRESSION_ATTACK_TIME:
+ *property = SI4713_TX_ACOMP_ATTACK_TIME;
+ *mul = ATTACK_TIME_UNIT;
+ break;
+ case V4L2_CID_PILOT_TONE_DEVIATION:
+ *property = SI4713_TX_PILOT_DEVIATION;
+ *mul = 10;
+ break;
+ case V4L2_CID_AUDIO_LIMITER_DEVIATION:
+ *property = SI4713_TX_AUDIO_DEVIATION;
+ *mul = 10;
+ break;
+ case V4L2_CID_RDS_TX_DEVIATION:
+ *property = SI4713_TX_RDS_DEVIATION;
+ *mul = 1;
+ break;
+
+ case V4L2_CID_RDS_TX_PTY:
+ *property = SI4713_TX_RDS_PS_MISC;
+ *bit = 5;
+ *mask = 0x1F << 5;
+ break;
+ case V4L2_CID_RDS_TX_DYNAMIC_PTY:
+ *property = SI4713_TX_RDS_PS_MISC;
+ *bit = 15;
+ *mask = 1 << 15;
+ break;
+ case V4L2_CID_RDS_TX_COMPRESSED:
+ *property = SI4713_TX_RDS_PS_MISC;
+ *bit = 14;
+ *mask = 1 << 14;
+ break;
+ case V4L2_CID_RDS_TX_ARTIFICIAL_HEAD:
+ *property = SI4713_TX_RDS_PS_MISC;
+ *bit = 13;
+ *mask = 1 << 13;
+ break;
+ case V4L2_CID_RDS_TX_MONO_STEREO:
+ *property = SI4713_TX_RDS_PS_MISC;
+ *bit = 12;
+ *mask = 1 << 12;
+ break;
+ case V4L2_CID_RDS_TX_TRAFFIC_PROGRAM:
+ *property = SI4713_TX_RDS_PS_MISC;
+ *bit = 10;
+ *mask = 1 << 10;
+ break;
+ case V4L2_CID_RDS_TX_TRAFFIC_ANNOUNCEMENT:
+ *property = SI4713_TX_RDS_PS_MISC;
+ *bit = 4;
+ *mask = 1 << 4;
+ break;
+ case V4L2_CID_RDS_TX_MUSIC_SPEECH:
+ *property = SI4713_TX_RDS_PS_MISC;
+ *bit = 3;
+ *mask = 1 << 3;
+ break;
+ case V4L2_CID_AUDIO_LIMITER_ENABLED:
+ *property = SI4713_TX_ACOMP_ENABLE;
+ *bit = 1;
+ *mask = 1 << 1;
+ break;
+ case V4L2_CID_AUDIO_COMPRESSION_ENABLED:
+ *property = SI4713_TX_ACOMP_ENABLE;
+ *bit = 0;
+ *mask = 1 << 0;
+ break;
+ case V4L2_CID_PILOT_TONE_ENABLED:
+ *property = SI4713_TX_COMPONENT_ENABLE;
+ *bit = 0;
+ *mask = 1 << 0;
+ break;
+
+ case V4L2_CID_AUDIO_LIMITER_RELEASE_TIME:
+ *property = SI4713_TX_LIMITER_RELEASE_TIME;
+ *table = limiter_times;
+ *size = ARRAY_SIZE(limiter_times);
+ break;
+ case V4L2_CID_AUDIO_COMPRESSION_RELEASE_TIME:
+ *property = SI4713_TX_ACOMP_RELEASE_TIME;
+ *table = acomp_rtimes;
+ *size = ARRAY_SIZE(acomp_rtimes);
+ break;
+ case V4L2_CID_TUNE_PREEMPHASIS:
+ *property = SI4713_TX_PREEMPHASIS;
+ *table = preemphasis_values;
+ *size = ARRAY_SIZE(preemphasis_values);
+ break;
+
+ default:
+ rval = -EINVAL;
+ break;
+ }
+
+ return rval;
+}
+
+static int si4713_s_frequency(struct v4l2_subdev *sd, const struct v4l2_frequency *f);
+static int si4713_s_modulator(struct v4l2_subdev *sd, const struct v4l2_modulator *);
+/*
+ * si4713_setup - Sets the device up with current configuration.
+ * @sdev: si4713_device structure for the device we are communicating
+ */
+static int si4713_setup(struct si4713_device *sdev)
+{
+ struct v4l2_frequency f;
+ struct v4l2_modulator vm;
+ int rval;
+
+ /* Device procedure needs to set frequency first */
+ f.tuner = 0;
+ f.frequency = sdev->frequency ? sdev->frequency : DEFAULT_FREQUENCY;
+ f.frequency = si4713_to_v4l2(f.frequency);
+ rval = si4713_s_frequency(&sdev->sd, &f);
+
+ vm.index = 0;
+ if (sdev->stereo)
+ vm.txsubchans = V4L2_TUNER_SUB_STEREO;
+ else
+ vm.txsubchans = V4L2_TUNER_SUB_MONO;
+ if (sdev->rds_enabled)
+ vm.txsubchans |= V4L2_TUNER_SUB_RDS;
+ si4713_s_modulator(&sdev->sd, &vm);
+
+ return rval;
+}
+
+/*
+ * si4713_initialize - Sets the device up with default configuration.
+ * @sdev: si4713_device structure for the device we are communicating
+ */
+static int si4713_initialize(struct si4713_device *sdev)
+{
+ int rval;
+
+ rval = si4713_set_power_state(sdev, POWER_ON);
+ if (rval < 0)
+ return rval;
+
+ rval = si4713_checkrev(sdev);
+ if (rval < 0)
+ return rval;
+
+ rval = si4713_set_power_state(sdev, POWER_OFF);
+ if (rval < 0)
+ return rval;
+
+ sdev->frequency = DEFAULT_FREQUENCY;
+ sdev->stereo = 1;
+ sdev->tune_rnl = DEFAULT_TUNE_RNL;
+ return 0;
+}
+
+/* si4713_s_ctrl - set the value of a control */
+static int si4713_s_ctrl(struct v4l2_ctrl *ctrl)
+{
+ struct si4713_device *sdev =
+ container_of(ctrl->handler, struct si4713_device, ctrl_handler);
+ u32 val = 0;
+ s32 bit = 0, mask = 0;
+ u16 property = 0;
+ int mul = 0;
+ unsigned long *table = NULL;
+ int size = 0;
+ bool force = false;
+ int c;
+ int ret = 0;
+
+ if (ctrl->id != V4L2_CID_AUDIO_MUTE)
+ return -EINVAL;
+ if (ctrl->is_new) {
+ if (ctrl->val) {
+ ret = si4713_set_mute(sdev, ctrl->val);
+ if (!ret)
+ ret = si4713_set_power_state(sdev, POWER_DOWN);
+ return ret;
+ }
+ ret = si4713_set_power_state(sdev, POWER_UP);
+ if (!ret)
+ ret = si4713_set_mute(sdev, ctrl->val);
+ if (!ret)
+ ret = si4713_setup(sdev);
+ if (ret)
+ return ret;
+ force = true;
+ }
+
+ if (!sdev->power_state)
+ return 0;
+
+ for (c = 1; !ret && c < ctrl->ncontrols; c++) {
+ ctrl = ctrl->cluster[c];
+
+ if (!force && !ctrl->is_new)
+ continue;
+
+ switch (ctrl->id) {
+ case V4L2_CID_RDS_TX_PS_NAME:
+ ret = si4713_set_rds_ps_name(sdev, ctrl->p_new.p_char);
+ break;
+
+ case V4L2_CID_RDS_TX_RADIO_TEXT:
+ ret = si4713_set_rds_radio_text(sdev, ctrl->p_new.p_char);
+ break;
+
+ case V4L2_CID_TUNE_ANTENNA_CAPACITOR:
+ /* don't handle this control if we force setting all
+ * controls since in that case it will be handled by
+ * V4L2_CID_TUNE_POWER_LEVEL. */
+ if (force)
+ break;
+ fallthrough;
+ case V4L2_CID_TUNE_POWER_LEVEL:
+ ret = si4713_tx_tune_power(sdev,
+ sdev->tune_pwr_level->val, sdev->tune_ant_cap->val);
+ if (!ret) {
+ /* Make sure we don't set this twice */
+ sdev->tune_ant_cap->is_new = false;
+ sdev->tune_pwr_level->is_new = false;
+ }
+ break;
+
+ case V4L2_CID_RDS_TX_ALT_FREQS_ENABLE:
+ case V4L2_CID_RDS_TX_ALT_FREQS:
+ if (sdev->rds_alt_freqs_enable->val) {
+ val = sdev->rds_alt_freqs->p_new.p_u32[0];
+ val = val / 100 - 876 + 0xe101;
+ } else {
+ val = 0xe0e0;
+ }
+ ret = si4713_write_property(sdev, SI4713_TX_RDS_PS_AF, val);
+ break;
+
+ default:
+ ret = si4713_choose_econtrol_action(sdev, ctrl->id, &bit,
+ &mask, &property, &mul, &table, &size);
+ if (ret < 0)
+ break;
+
+ val = ctrl->val;
+ if (mul) {
+ val = val / mul;
+ } else if (table) {
+ ret = usecs_to_dev(val, table, size);
+ if (ret < 0)
+ break;
+ val = ret;
+ ret = 0;
+ }
+
+ if (mask) {
+ ret = si4713_read_property(sdev, property, &val);
+ if (ret < 0)
+ break;
+ val = set_bits(val, ctrl->val, bit, mask);
+ }
+
+ ret = si4713_write_property(sdev, property, val);
+ if (ret < 0)
+ break;
+ if (mask)
+ val = ctrl->val;
+ break;
+ }
+ }
+
+ return ret;
+}
+
+/* si4713_ioctl - deal with private ioctls (only rnl for now) */
+static long si4713_ioctl(struct v4l2_subdev *sd, unsigned int cmd, void *arg)
+{
+ struct si4713_device *sdev = to_si4713_device(sd);
+ struct si4713_rnl *rnl = arg;
+ u16 frequency;
+ int rval = 0;
+
+ if (!arg)
+ return -EINVAL;
+
+ switch (cmd) {
+ case SI4713_IOC_MEASURE_RNL:
+ frequency = v4l2_to_si4713(rnl->frequency);
+
+ if (sdev->power_state) {
+ /* Set desired measurement frequency */
+ rval = si4713_tx_tune_measure(sdev, frequency, 0);
+ if (rval < 0)
+ return rval;
+ /* get results from tune status */
+ rval = si4713_update_tune_status(sdev);
+ if (rval < 0)
+ return rval;
+ }
+ rnl->rnl = sdev->tune_rnl;
+ break;
+
+ default:
+ /* nothing */
+ rval = -ENOIOCTLCMD;
+ }
+
+ return rval;
+}
+
+/* si4713_g_modulator - get modulator attributes */
+static int si4713_g_modulator(struct v4l2_subdev *sd, struct v4l2_modulator *vm)
+{
+ struct si4713_device *sdev = to_si4713_device(sd);
+ int rval = 0;
+
+ if (!sdev)
+ return -ENODEV;
+
+ if (vm->index > 0)
+ return -EINVAL;
+
+ strscpy(vm->name, "FM Modulator", sizeof(vm->name));
+ vm->capability = V4L2_TUNER_CAP_STEREO | V4L2_TUNER_CAP_LOW |
+ V4L2_TUNER_CAP_RDS | V4L2_TUNER_CAP_RDS_CONTROLS;
+
+ /* Report current frequency range limits */
+ vm->rangelow = si4713_to_v4l2(FREQ_RANGE_LOW);
+ vm->rangehigh = si4713_to_v4l2(FREQ_RANGE_HIGH);
+
+ if (sdev->power_state) {
+ u32 comp_en = 0;
+
+ rval = si4713_read_property(sdev, SI4713_TX_COMPONENT_ENABLE,
+ &comp_en);
+ if (rval < 0)
+ return rval;
+
+ sdev->stereo = get_status_bit(comp_en, 1, 1 << 1);
+ }
+
+ /* Report current audio mode: mono or stereo */
+ if (sdev->stereo)
+ vm->txsubchans = V4L2_TUNER_SUB_STEREO;
+ else
+ vm->txsubchans = V4L2_TUNER_SUB_MONO;
+
+ /* Report rds feature status */
+ if (sdev->rds_enabled)
+ vm->txsubchans |= V4L2_TUNER_SUB_RDS;
+ else
+ vm->txsubchans &= ~V4L2_TUNER_SUB_RDS;
+
+ return rval;
+}
+
+/* si4713_s_modulator - set modulator attributes */
+static int si4713_s_modulator(struct v4l2_subdev *sd, const struct v4l2_modulator *vm)
+{
+ struct si4713_device *sdev = to_si4713_device(sd);
+ int rval = 0;
+ u16 stereo, rds;
+ u32 p;
+
+ if (!sdev)
+ return -ENODEV;
+
+ if (vm->index > 0)
+ return -EINVAL;
+
+ /* Set audio mode: mono or stereo */
+ if (vm->txsubchans & V4L2_TUNER_SUB_STEREO)
+ stereo = 1;
+ else if (vm->txsubchans & V4L2_TUNER_SUB_MONO)
+ stereo = 0;
+ else
+ return -EINVAL;
+
+ rds = !!(vm->txsubchans & V4L2_TUNER_SUB_RDS);
+
+ if (sdev->power_state) {
+ rval = si4713_read_property(sdev,
+ SI4713_TX_COMPONENT_ENABLE, &p);
+ if (rval < 0)
+ return rval;
+
+ p = set_bits(p, stereo, 1, 1 << 1);
+ p = set_bits(p, rds, 2, 1 << 2);
+
+ rval = si4713_write_property(sdev,
+ SI4713_TX_COMPONENT_ENABLE, p);
+ if (rval < 0)
+ return rval;
+ }
+
+ sdev->stereo = stereo;
+ sdev->rds_enabled = rds;
+
+ return rval;
+}
+
+/* si4713_g_frequency - get tuner or modulator radio frequency */
+static int si4713_g_frequency(struct v4l2_subdev *sd, struct v4l2_frequency *f)
+{
+ struct si4713_device *sdev = to_si4713_device(sd);
+ int rval = 0;
+
+ if (f->tuner)
+ return -EINVAL;
+
+ if (sdev->power_state) {
+ u16 freq;
+ u8 p, a, n;
+
+ rval = si4713_tx_tune_status(sdev, 0x00, &freq, &p, &a, &n);
+ if (rval < 0)
+ return rval;
+
+ sdev->frequency = freq;
+ }
+
+ f->frequency = si4713_to_v4l2(sdev->frequency);
+
+ return rval;
+}
+
+/* si4713_s_frequency - set tuner or modulator radio frequency */
+static int si4713_s_frequency(struct v4l2_subdev *sd, const struct v4l2_frequency *f)
+{
+ struct si4713_device *sdev = to_si4713_device(sd);
+ int rval = 0;
+ u16 frequency = v4l2_to_si4713(f->frequency);
+
+ if (f->tuner)
+ return -EINVAL;
+
+ /* Check frequency range */
+ frequency = clamp_t(u16, frequency, FREQ_RANGE_LOW, FREQ_RANGE_HIGH);
+
+ if (sdev->power_state) {
+ rval = si4713_tx_tune_freq(sdev, frequency);
+ if (rval < 0)
+ return rval;
+ frequency = rval;
+ rval = 0;
+ }
+ sdev->frequency = frequency;
+
+ return rval;
+}
+
+static const struct v4l2_ctrl_ops si4713_ctrl_ops = {
+ .s_ctrl = si4713_s_ctrl,
+};
+
+static const struct v4l2_subdev_core_ops si4713_subdev_core_ops = {
+ .ioctl = si4713_ioctl,
+};
+
+static const struct v4l2_subdev_tuner_ops si4713_subdev_tuner_ops = {
+ .g_frequency = si4713_g_frequency,
+ .s_frequency = si4713_s_frequency,
+ .g_modulator = si4713_g_modulator,
+ .s_modulator = si4713_s_modulator,
+};
+
+static const struct v4l2_subdev_ops si4713_subdev_ops = {
+ .core = &si4713_subdev_core_ops,
+ .tuner = &si4713_subdev_tuner_ops,
+};
+
+static const struct v4l2_ctrl_config si4713_alt_freqs_ctrl = {
+ .id = V4L2_CID_RDS_TX_ALT_FREQS,
+ .type = V4L2_CTRL_TYPE_U32,
+ .min = 87600,
+ .max = 107900,
+ .step = 100,
+ .def = 87600,
+ .dims = { 1 },
+ .elem_size = sizeof(u32),
+};
+
+/*
+ * I2C driver interface
+ */
+/* si4713_probe - probe for the device */
+static int si4713_probe(struct i2c_client *client)
+{
+ struct si4713_device *sdev;
+ struct v4l2_ctrl_handler *hdl;
+ struct si4713_platform_data *pdata = client->dev.platform_data;
+ struct device_node *np = client->dev.of_node;
+ struct radio_si4713_platform_data si4713_pdev_pdata;
+ struct platform_device *si4713_pdev;
+ int rval;
+
+ sdev = devm_kzalloc(&client->dev, sizeof(*sdev), GFP_KERNEL);
+ if (!sdev) {
+ dev_err(&client->dev, "Failed to alloc video device.\n");
+ rval = -ENOMEM;
+ goto exit;
+ }
+
+ sdev->gpio_reset = devm_gpiod_get_optional(&client->dev, "reset",
+ GPIOD_OUT_LOW);
+ if (IS_ERR(sdev->gpio_reset)) {
+ rval = PTR_ERR(sdev->gpio_reset);
+ dev_err(&client->dev, "Failed to request gpio: %d\n", rval);
+ goto exit;
+ }
+
+ sdev->vdd = devm_regulator_get_optional(&client->dev, "vdd");
+ if (IS_ERR(sdev->vdd)) {
+ rval = PTR_ERR(sdev->vdd);
+ if (rval == -EPROBE_DEFER)
+ goto exit;
+
+ dev_dbg(&client->dev, "no vdd regulator found: %d\n", rval);
+ sdev->vdd = NULL;
+ }
+
+ sdev->vio = devm_regulator_get_optional(&client->dev, "vio");
+ if (IS_ERR(sdev->vio)) {
+ rval = PTR_ERR(sdev->vio);
+ if (rval == -EPROBE_DEFER)
+ goto exit;
+
+ dev_dbg(&client->dev, "no vio regulator found: %d\n", rval);
+ sdev->vio = NULL;
+ }
+
+ v4l2_i2c_subdev_init(&sdev->sd, client, &si4713_subdev_ops);
+
+ init_completion(&sdev->work);
+
+ hdl = &sdev->ctrl_handler;
+ v4l2_ctrl_handler_init(hdl, 20);
+ sdev->mute = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_AUDIO_MUTE, 0, 1, 1, DEFAULT_MUTE);
+
+ sdev->rds_pi = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_RDS_TX_PI, 0, 0xffff, 1, DEFAULT_RDS_PI);
+ sdev->rds_pty = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_RDS_TX_PTY, 0, 31, 1, DEFAULT_RDS_PTY);
+ sdev->rds_compressed = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_RDS_TX_COMPRESSED, 0, 1, 1, 0);
+ sdev->rds_art_head = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_RDS_TX_ARTIFICIAL_HEAD, 0, 1, 1, 0);
+ sdev->rds_stereo = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_RDS_TX_MONO_STEREO, 0, 1, 1, 1);
+ sdev->rds_tp = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_RDS_TX_TRAFFIC_PROGRAM, 0, 1, 1, 0);
+ sdev->rds_ta = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_RDS_TX_TRAFFIC_ANNOUNCEMENT, 0, 1, 1, 0);
+ sdev->rds_ms = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_RDS_TX_MUSIC_SPEECH, 0, 1, 1, 1);
+ sdev->rds_dyn_pty = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_RDS_TX_DYNAMIC_PTY, 0, 1, 1, 0);
+ sdev->rds_alt_freqs_enable = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_RDS_TX_ALT_FREQS_ENABLE, 0, 1, 1, 0);
+ sdev->rds_alt_freqs = v4l2_ctrl_new_custom(hdl, &si4713_alt_freqs_ctrl, NULL);
+ sdev->rds_deviation = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_RDS_TX_DEVIATION, 0, MAX_RDS_DEVIATION,
+ 10, DEFAULT_RDS_DEVIATION);
+ /*
+ * Report step as 8. From RDS spec, psname
+ * should be 8. But there are receivers which scroll strings
+ * sized as 8xN.
+ */
+ sdev->rds_ps_name = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_RDS_TX_PS_NAME, 0, MAX_RDS_PS_NAME, 8, 0);
+ /*
+ * Report step as 32 (2A block). From RDS spec,
+ * radio text should be 32 for 2A block. But there are receivers
+ * which scroll strings sized as 32xN. Setting default to 32.
+ */
+ sdev->rds_radio_text = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_RDS_TX_RADIO_TEXT, 0, MAX_RDS_RADIO_TEXT, 32, 0);
+
+ sdev->limiter_enabled = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_AUDIO_LIMITER_ENABLED, 0, 1, 1, 1);
+ sdev->limiter_release_time = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_AUDIO_LIMITER_RELEASE_TIME, 250,
+ MAX_LIMITER_RELEASE_TIME, 10, DEFAULT_LIMITER_RTIME);
+ sdev->limiter_deviation = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_AUDIO_LIMITER_DEVIATION, 0,
+ MAX_LIMITER_DEVIATION, 10, DEFAULT_LIMITER_DEV);
+
+ sdev->compression_enabled = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_AUDIO_COMPRESSION_ENABLED, 0, 1, 1, 1);
+ sdev->compression_gain = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_AUDIO_COMPRESSION_GAIN, 0, MAX_ACOMP_GAIN, 1,
+ DEFAULT_ACOMP_GAIN);
+ sdev->compression_threshold = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_AUDIO_COMPRESSION_THRESHOLD,
+ MIN_ACOMP_THRESHOLD, MAX_ACOMP_THRESHOLD, 1,
+ DEFAULT_ACOMP_THRESHOLD);
+ sdev->compression_attack_time = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_AUDIO_COMPRESSION_ATTACK_TIME, 0,
+ MAX_ACOMP_ATTACK_TIME, 500, DEFAULT_ACOMP_ATIME);
+ sdev->compression_release_time = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_AUDIO_COMPRESSION_RELEASE_TIME, 100000,
+ MAX_ACOMP_RELEASE_TIME, 100000, DEFAULT_ACOMP_RTIME);
+
+ sdev->pilot_tone_enabled = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_PILOT_TONE_ENABLED, 0, 1, 1, 1);
+ sdev->pilot_tone_deviation = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_PILOT_TONE_DEVIATION, 0, MAX_PILOT_DEVIATION,
+ 10, DEFAULT_PILOT_DEVIATION);
+ sdev->pilot_tone_freq = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_PILOT_TONE_FREQUENCY, 0, MAX_PILOT_FREQUENCY,
+ 1, DEFAULT_PILOT_FREQUENCY);
+
+ sdev->tune_preemphasis = v4l2_ctrl_new_std_menu(hdl, &si4713_ctrl_ops,
+ V4L2_CID_TUNE_PREEMPHASIS,
+ V4L2_PREEMPHASIS_75_uS, 0, V4L2_PREEMPHASIS_50_uS);
+ sdev->tune_pwr_level = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_TUNE_POWER_LEVEL, 0, SI4713_MAX_POWER,
+ 1, DEFAULT_POWER_LEVEL);
+ sdev->tune_ant_cap = v4l2_ctrl_new_std(hdl, &si4713_ctrl_ops,
+ V4L2_CID_TUNE_ANTENNA_CAPACITOR, 0, SI4713_MAX_ANTCAP,
+ 1, 0);
+
+ if (hdl->error) {
+ rval = hdl->error;
+ goto free_ctrls;
+ }
+ v4l2_ctrl_cluster(29, &sdev->mute);
+ sdev->sd.ctrl_handler = hdl;
+
+ if (client->irq) {
+ rval = devm_request_irq(&client->dev, client->irq,
+ si4713_handler, IRQF_TRIGGER_FALLING,
+ client->name, sdev);
+ if (rval < 0) {
+ v4l2_err(&sdev->sd, "Could not request IRQ\n");
+ goto free_ctrls;
+ }
+ v4l2_dbg(1, debug, &sdev->sd, "IRQ requested.\n");
+ } else {
+ v4l2_warn(&sdev->sd, "IRQ not configured. Using timeouts.\n");
+ }
+
+ rval = si4713_initialize(sdev);
+ if (rval < 0) {
+ v4l2_err(&sdev->sd, "Failed to probe device information.\n");
+ goto free_ctrls;
+ }
+
+ if (!np && (!pdata || !pdata->is_platform_device))
+ return 0;
+
+ si4713_pdev = platform_device_alloc("radio-si4713", -1);
+ if (!si4713_pdev) {
+ rval = -ENOMEM;
+ goto put_main_pdev;
+ }
+
+ si4713_pdev_pdata.subdev = client;
+ rval = platform_device_add_data(si4713_pdev, &si4713_pdev_pdata,
+ sizeof(si4713_pdev_pdata));
+ if (rval)
+ goto put_main_pdev;
+
+ rval = platform_device_add(si4713_pdev);
+ if (rval)
+ goto put_main_pdev;
+
+ sdev->pd = si4713_pdev;
+
+ return 0;
+
+put_main_pdev:
+ platform_device_put(si4713_pdev);
+ v4l2_device_unregister_subdev(&sdev->sd);
+free_ctrls:
+ v4l2_ctrl_handler_free(hdl);
+exit:
+ return rval;
+}
+
+/* si4713_remove - remove the device */
+static void si4713_remove(struct i2c_client *client)
+{
+ struct v4l2_subdev *sd = i2c_get_clientdata(client);
+ struct si4713_device *sdev = to_si4713_device(sd);
+
+ platform_device_unregister(sdev->pd);
+
+ if (sdev->power_state)
+ si4713_set_power_state(sdev, POWER_DOWN);
+
+ v4l2_device_unregister_subdev(sd);
+ v4l2_ctrl_handler_free(sd->ctrl_handler);
+}
+
+/* si4713_i2c_driver - i2c driver interface */
+static const struct i2c_device_id si4713_id[] = {
+ { .name = "si4713" },
+ { }
+};
+MODULE_DEVICE_TABLE(i2c, si4713_id);
+
+#if IS_ENABLED(CONFIG_OF)
+static const struct of_device_id si4713_of_match[] = {
+ { .compatible = "silabs,si4713" },
+ { },
+};
+MODULE_DEVICE_TABLE(of, si4713_of_match);
+#endif
+
+static struct i2c_driver si4713_i2c_driver = {
+ .driver = {
+ .name = "si4713",
+ .of_match_table = of_match_ptr(si4713_of_match),
+ },
+ .probe = si4713_probe,
+ .remove = si4713_remove,
+ .id_table = si4713_id,
+};
+
+module_i2c_driver(si4713_i2c_driver);
diff --git a/drivers/media/radio/si4713/si4713.h b/drivers/media/radio/si4713/si4713.h
new file mode 100644
index 000000000..29d0e1f10
--- /dev/null
+++ b/drivers/media/radio/si4713/si4713.h
@@ -0,0 +1,254 @@
+/*
+ * drivers/media/radio/si4713-i2c.h
+ *
+ * Property and commands definitions for Si4713 radio transmitter chip.
+ *
+ * Copyright (c) 2008 Instituto Nokia de Tecnologia - INdT
+ * Contact: Eduardo Valentin <eduardo.valentin@nokia.com>
+ *
+ * This file is licensed under the terms of the GNU General Public License
+ * version 2. This program is licensed "as is" without any warranty of any
+ * kind, whether express or implied.
+ *
+ */
+
+#ifndef SI4713_I2C_H
+#define SI4713_I2C_H
+
+#include <linux/platform_device.h>
+#include <linux/regulator/consumer.h>
+#include <linux/gpio/consumer.h>
+#include <media/v4l2-subdev.h>
+#include <media/v4l2-ctrls.h>
+#include <linux/platform_data/media/si4713.h>
+
+#define SI4713_PRODUCT_NUMBER 0x0D
+
+/* Command Timeouts */
+#define DEFAULT_TIMEOUT 500
+#define TIMEOUT_SET_PROPERTY 20
+#define TIMEOUT_TX_TUNE_POWER 30000
+#define TIMEOUT_TX_TUNE 110000
+#define TIMEOUT_POWER_UP 200000
+
+/*
+ * Command and its arguments definitions
+ */
+#define SI4713_PWUP_CTSIEN (1<<7)
+#define SI4713_PWUP_GPO2OEN (1<<6)
+#define SI4713_PWUP_PATCH (1<<5)
+#define SI4713_PWUP_XOSCEN (1<<4)
+#define SI4713_PWUP_FUNC_TX 0x02
+#define SI4713_PWUP_FUNC_PATCH 0x0F
+#define SI4713_PWUP_OPMOD_ANALOG 0x50
+#define SI4713_PWUP_OPMOD_DIGITAL 0x0F
+#define SI4713_PWUP_NARGS 2
+#define SI4713_PWUP_NRESP 1
+#define SI4713_CMD_POWER_UP 0x01
+
+#define SI4713_GETREV_NRESP 9
+#define SI4713_CMD_GET_REV 0x10
+
+#define SI4713_PWDN_NRESP 1
+#define SI4713_CMD_POWER_DOWN 0x11
+
+#define SI4713_SET_PROP_NARGS 5
+#define SI4713_SET_PROP_NRESP 1
+#define SI4713_CMD_SET_PROPERTY 0x12
+
+#define SI4713_GET_PROP_NARGS 3
+#define SI4713_GET_PROP_NRESP 4
+#define SI4713_CMD_GET_PROPERTY 0x13
+
+#define SI4713_GET_STATUS_NRESP 1
+#define SI4713_CMD_GET_INT_STATUS 0x14
+
+#define SI4713_CMD_PATCH_ARGS 0x15
+#define SI4713_CMD_PATCH_DATA 0x16
+
+#define SI4713_MAX_FREQ 10800
+#define SI4713_MIN_FREQ 7600
+#define SI4713_TXFREQ_NARGS 3
+#define SI4713_TXFREQ_NRESP 1
+#define SI4713_CMD_TX_TUNE_FREQ 0x30
+
+#define SI4713_MAX_POWER 120
+#define SI4713_MIN_POWER 88
+#define SI4713_MAX_ANTCAP 191
+#define SI4713_MIN_ANTCAP 0
+#define SI4713_TXPWR_NARGS 4
+#define SI4713_TXPWR_NRESP 1
+#define SI4713_CMD_TX_TUNE_POWER 0x31
+
+#define SI4713_TXMEA_NARGS 4
+#define SI4713_TXMEA_NRESP 1
+#define SI4713_CMD_TX_TUNE_MEASURE 0x32
+
+#define SI4713_INTACK_MASK 0x01
+#define SI4713_TXSTATUS_NARGS 1
+#define SI4713_TXSTATUS_NRESP 8
+#define SI4713_CMD_TX_TUNE_STATUS 0x33
+
+#define SI4713_OVERMOD_BIT (1 << 2)
+#define SI4713_IALH_BIT (1 << 1)
+#define SI4713_IALL_BIT (1 << 0)
+#define SI4713_ASQSTATUS_NARGS 1
+#define SI4713_ASQSTATUS_NRESP 5
+#define SI4713_CMD_TX_ASQ_STATUS 0x34
+
+#define SI4713_RDSBUFF_MODE_MASK 0x87
+#define SI4713_RDSBUFF_NARGS 7
+#define SI4713_RDSBUFF_NRESP 6
+#define SI4713_CMD_TX_RDS_BUFF 0x35
+
+#define SI4713_RDSPS_PSID_MASK 0x1F
+#define SI4713_RDSPS_NARGS 5
+#define SI4713_RDSPS_NRESP 1
+#define SI4713_CMD_TX_RDS_PS 0x36
+
+#define SI4713_CMD_GPO_CTL 0x80
+#define SI4713_CMD_GPO_SET 0x81
+
+/*
+ * Bits from status response
+ */
+#define SI4713_CTS (1<<7)
+#define SI4713_ERR (1<<6)
+#define SI4713_RDS_INT (1<<2)
+#define SI4713_ASQ_INT (1<<1)
+#define SI4713_STC_INT (1<<0)
+
+/*
+ * Property definitions
+ */
+#define SI4713_GPO_IEN 0x0001
+#define SI4713_DIG_INPUT_FORMAT 0x0101
+#define SI4713_DIG_INPUT_SAMPLE_RATE 0x0103
+#define SI4713_REFCLK_FREQ 0x0201
+#define SI4713_REFCLK_PRESCALE 0x0202
+#define SI4713_TX_COMPONENT_ENABLE 0x2100
+#define SI4713_TX_AUDIO_DEVIATION 0x2101
+#define SI4713_TX_PILOT_DEVIATION 0x2102
+#define SI4713_TX_RDS_DEVIATION 0x2103
+#define SI4713_TX_LINE_INPUT_LEVEL 0x2104
+#define SI4713_TX_LINE_INPUT_MUTE 0x2105
+#define SI4713_TX_PREEMPHASIS 0x2106
+#define SI4713_TX_PILOT_FREQUENCY 0x2107
+#define SI4713_TX_ACOMP_ENABLE 0x2200
+#define SI4713_TX_ACOMP_THRESHOLD 0x2201
+#define SI4713_TX_ACOMP_ATTACK_TIME 0x2202
+#define SI4713_TX_ACOMP_RELEASE_TIME 0x2203
+#define SI4713_TX_ACOMP_GAIN 0x2204
+#define SI4713_TX_LIMITER_RELEASE_TIME 0x2205
+#define SI4713_TX_ASQ_INTERRUPT_SOURCE 0x2300
+#define SI4713_TX_ASQ_LEVEL_LOW 0x2301
+#define SI4713_TX_ASQ_DURATION_LOW 0x2302
+#define SI4713_TX_ASQ_LEVEL_HIGH 0x2303
+#define SI4713_TX_ASQ_DURATION_HIGH 0x2304
+#define SI4713_TX_RDS_INTERRUPT_SOURCE 0x2C00
+#define SI4713_TX_RDS_PI 0x2C01
+#define SI4713_TX_RDS_PS_MIX 0x2C02
+#define SI4713_TX_RDS_PS_MISC 0x2C03
+#define SI4713_TX_RDS_PS_REPEAT_COUNT 0x2C04
+#define SI4713_TX_RDS_PS_MESSAGE_COUNT 0x2C05
+#define SI4713_TX_RDS_PS_AF 0x2C06
+#define SI4713_TX_RDS_FIFO_SIZE 0x2C07
+
+#define PREEMPHASIS_USA 75
+#define PREEMPHASIS_EU 50
+#define PREEMPHASIS_DISABLED 0
+#define FMPE_USA 0x00
+#define FMPE_EU 0x01
+#define FMPE_DISABLED 0x02
+
+#define POWER_UP 0x01
+#define POWER_DOWN 0x00
+
+#define MAX_RDS_PTY 31
+#define MAX_RDS_DEVIATION 90000
+
+/*
+ * PSNAME is known to be defined as 8 character sized (RDS Spec).
+ * However, there is receivers which scroll PSNAME 8xN sized.
+ */
+#define MAX_RDS_PS_NAME 96
+
+/*
+ * MAX_RDS_RADIO_TEXT is known to be defined as 32 (2A group) or 64 (2B group)
+ * character sized (RDS Spec).
+ * However, there is receivers which scroll them as well.
+ */
+#define MAX_RDS_RADIO_TEXT 384
+
+#define MAX_LIMITER_RELEASE_TIME 102390
+#define MAX_LIMITER_DEVIATION 90000
+
+#define MAX_PILOT_DEVIATION 90000
+#define MAX_PILOT_FREQUENCY 19000
+
+#define MAX_ACOMP_RELEASE_TIME 1000000
+#define MAX_ACOMP_ATTACK_TIME 5000
+#define MAX_ACOMP_THRESHOLD 0
+#define MIN_ACOMP_THRESHOLD (-40)
+#define MAX_ACOMP_GAIN 20
+
+/*
+ * si4713_device - private data
+ */
+struct si4713_device {
+ /* v4l2_subdev and i2c reference (v4l2_subdev priv data) */
+ struct v4l2_subdev sd;
+ struct v4l2_ctrl_handler ctrl_handler;
+ /* private data structures */
+ struct { /* si4713 control cluster */
+ /* This is one big cluster since the mute control
+ * powers off the device and after unmuting again all
+ * controls need to be set at once. The only way of doing
+ * that is by making it one big cluster. */
+ struct v4l2_ctrl *mute;
+ struct v4l2_ctrl *rds_ps_name;
+ struct v4l2_ctrl *rds_radio_text;
+ struct v4l2_ctrl *rds_pi;
+ struct v4l2_ctrl *rds_deviation;
+ struct v4l2_ctrl *rds_pty;
+ struct v4l2_ctrl *rds_compressed;
+ struct v4l2_ctrl *rds_art_head;
+ struct v4l2_ctrl *rds_stereo;
+ struct v4l2_ctrl *rds_ta;
+ struct v4l2_ctrl *rds_tp;
+ struct v4l2_ctrl *rds_ms;
+ struct v4l2_ctrl *rds_dyn_pty;
+ struct v4l2_ctrl *rds_alt_freqs_enable;
+ struct v4l2_ctrl *rds_alt_freqs;
+ struct v4l2_ctrl *compression_enabled;
+ struct v4l2_ctrl *compression_threshold;
+ struct v4l2_ctrl *compression_gain;
+ struct v4l2_ctrl *compression_attack_time;
+ struct v4l2_ctrl *compression_release_time;
+ struct v4l2_ctrl *pilot_tone_enabled;
+ struct v4l2_ctrl *pilot_tone_freq;
+ struct v4l2_ctrl *pilot_tone_deviation;
+ struct v4l2_ctrl *limiter_enabled;
+ struct v4l2_ctrl *limiter_deviation;
+ struct v4l2_ctrl *limiter_release_time;
+ struct v4l2_ctrl *tune_preemphasis;
+ struct v4l2_ctrl *tune_pwr_level;
+ struct v4l2_ctrl *tune_ant_cap;
+ };
+ struct completion work;
+ struct regulator *vdd;
+ struct regulator *vio;
+ struct gpio_desc *gpio_reset;
+ struct platform_device *pd;
+ u32 power_state;
+ u32 rds_enabled;
+ u32 frequency;
+ u32 preemphasis;
+ u32 stereo;
+ u32 tune_rnl;
+};
+
+struct radio_si4713_platform_data {
+ struct i2c_client *subdev;
+};
+#endif /* ifndef SI4713_I2C_H */
diff --git a/drivers/media/radio/tea575x.c b/drivers/media/radio/tea575x.c
new file mode 100644
index 000000000..c37315226
--- /dev/null
+++ b/drivers/media/radio/tea575x.c
@@ -0,0 +1,577 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * ALSA driver for TEA5757/5759 Philips AM/FM radio tuner chips
+ *
+ * Copyright (c) 2004 Jaroslav Kysela <perex@perex.cz>
+ */
+
+#include <linux/delay.h>
+#include <linux/module.h>
+#include <linux/init.h>
+#include <linux/slab.h>
+#include <linux/sched.h>
+#include <asm/io.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-dev.h>
+#include <media/v4l2-fh.h>
+#include <media/v4l2-ioctl.h>
+#include <media/v4l2-event.h>
+#include <media/drv-intf/tea575x.h>
+
+MODULE_AUTHOR("Jaroslav Kysela <perex@perex.cz>");
+MODULE_DESCRIPTION("Routines for control of TEA5757/5759 Philips AM/FM radio tuner chips");
+MODULE_LICENSE("GPL");
+
+/*
+ * definitions
+ */
+
+#define TEA575X_BIT_SEARCH (1<<24) /* 1 = search action, 0 = tuned */
+#define TEA575X_BIT_UPDOWN (1<<23) /* 0 = search down, 1 = search up */
+#define TEA575X_BIT_MONO (1<<22) /* 0 = stereo, 1 = mono */
+#define TEA575X_BIT_BAND_MASK (3<<20)
+#define TEA575X_BIT_BAND_FM (0<<20)
+#define TEA575X_BIT_BAND_MW (1<<20)
+#define TEA575X_BIT_BAND_LW (2<<20)
+#define TEA575X_BIT_BAND_SW (3<<20)
+#define TEA575X_BIT_PORT_0 (1<<19) /* user bit */
+#define TEA575X_BIT_PORT_1 (1<<18) /* user bit */
+#define TEA575X_BIT_SEARCH_MASK (3<<16) /* search level */
+#define TEA575X_BIT_SEARCH_5_28 (0<<16) /* FM >5uV, AM >28uV */
+#define TEA575X_BIT_SEARCH_10_40 (1<<16) /* FM >10uV, AM > 40uV */
+#define TEA575X_BIT_SEARCH_30_63 (2<<16) /* FM >30uV, AM > 63uV */
+#define TEA575X_BIT_SEARCH_150_1000 (3<<16) /* FM > 150uV, AM > 1000uV */
+#define TEA575X_BIT_DUMMY (1<<15) /* buffer */
+#define TEA575X_BIT_FREQ_MASK 0x7fff
+
+enum { BAND_FM, BAND_FM_JAPAN, BAND_AM };
+
+static const struct v4l2_frequency_band bands[] = {
+ {
+ .type = V4L2_TUNER_RADIO,
+ .index = 0,
+ .capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO |
+ V4L2_TUNER_CAP_FREQ_BANDS,
+ .rangelow = 87500 * 16,
+ .rangehigh = 108000 * 16,
+ .modulation = V4L2_BAND_MODULATION_FM,
+ },
+ {
+ .type = V4L2_TUNER_RADIO,
+ .index = 0,
+ .capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO |
+ V4L2_TUNER_CAP_FREQ_BANDS,
+ .rangelow = 76000 * 16,
+ .rangehigh = 91000 * 16,
+ .modulation = V4L2_BAND_MODULATION_FM,
+ },
+ {
+ .type = V4L2_TUNER_RADIO,
+ .index = 1,
+ .capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_FREQ_BANDS,
+ .rangelow = 530 * 16,
+ .rangehigh = 1710 * 16,
+ .modulation = V4L2_BAND_MODULATION_AM,
+ },
+};
+
+/*
+ * lowlevel part
+ */
+
+static void snd_tea575x_write(struct snd_tea575x *tea, unsigned int val)
+{
+ u16 l;
+ u8 data;
+
+ if (tea->ops->write_val)
+ return tea->ops->write_val(tea, val);
+
+ tea->ops->set_direction(tea, 1);
+ udelay(16);
+
+ for (l = 25; l > 0; l--) {
+ data = (val >> 24) & TEA575X_DATA;
+ val <<= 1; /* shift data */
+ tea->ops->set_pins(tea, data | TEA575X_WREN);
+ udelay(2);
+ tea->ops->set_pins(tea, data | TEA575X_WREN | TEA575X_CLK);
+ udelay(2);
+ tea->ops->set_pins(tea, data | TEA575X_WREN);
+ udelay(2);
+ }
+
+ if (!tea->mute)
+ tea->ops->set_pins(tea, 0);
+}
+
+static u32 snd_tea575x_read(struct snd_tea575x *tea)
+{
+ u16 l, rdata;
+ u32 data = 0;
+
+ if (tea->ops->read_val)
+ return tea->ops->read_val(tea);
+
+ tea->ops->set_direction(tea, 0);
+ tea->ops->set_pins(tea, 0);
+ udelay(16);
+
+ for (l = 24; l--;) {
+ tea->ops->set_pins(tea, TEA575X_CLK);
+ udelay(2);
+ if (!l)
+ tea->tuned = tea->ops->get_pins(tea) & TEA575X_MOST ? 0 : 1;
+ tea->ops->set_pins(tea, 0);
+ udelay(2);
+ data <<= 1; /* shift data */
+ rdata = tea->ops->get_pins(tea);
+ if (!l)
+ tea->stereo = (rdata & TEA575X_MOST) ? 0 : 1;
+ if (rdata & TEA575X_DATA)
+ data++;
+ udelay(2);
+ }
+
+ if (tea->mute)
+ tea->ops->set_pins(tea, TEA575X_WREN);
+
+ return data;
+}
+
+static u32 snd_tea575x_val_to_freq(struct snd_tea575x *tea, u32 val)
+{
+ u32 freq = val & TEA575X_BIT_FREQ_MASK;
+
+ if (freq == 0)
+ return freq;
+
+ switch (tea->band) {
+ case BAND_FM:
+ /* freq *= 12.5 */
+ freq *= 125;
+ freq /= 10;
+ /* crystal fixup */
+ freq -= TEA575X_FMIF;
+ break;
+ case BAND_FM_JAPAN:
+ /* freq *= 12.5 */
+ freq *= 125;
+ freq /= 10;
+ /* crystal fixup */
+ freq += TEA575X_FMIF;
+ break;
+ case BAND_AM:
+ /* crystal fixup */
+ freq -= TEA575X_AMIF;
+ break;
+ }
+
+ return clamp(freq * 16, bands[tea->band].rangelow,
+ bands[tea->band].rangehigh); /* from kHz */
+}
+
+static u32 snd_tea575x_get_freq(struct snd_tea575x *tea)
+{
+ return snd_tea575x_val_to_freq(tea, snd_tea575x_read(tea));
+}
+
+void snd_tea575x_set_freq(struct snd_tea575x *tea)
+{
+ u32 freq = tea->freq / 16; /* to kHz */
+ u32 band = 0;
+
+ switch (tea->band) {
+ case BAND_FM:
+ band = TEA575X_BIT_BAND_FM;
+ /* crystal fixup */
+ freq += TEA575X_FMIF;
+ /* freq /= 12.5 */
+ freq *= 10;
+ freq /= 125;
+ break;
+ case BAND_FM_JAPAN:
+ band = TEA575X_BIT_BAND_FM;
+ /* crystal fixup */
+ freq -= TEA575X_FMIF;
+ /* freq /= 12.5 */
+ freq *= 10;
+ freq /= 125;
+ break;
+ case BAND_AM:
+ band = TEA575X_BIT_BAND_MW;
+ /* crystal fixup */
+ freq += TEA575X_AMIF;
+ break;
+ }
+
+ tea->val &= ~(TEA575X_BIT_FREQ_MASK | TEA575X_BIT_BAND_MASK);
+ tea->val |= band;
+ tea->val |= freq & TEA575X_BIT_FREQ_MASK;
+ snd_tea575x_write(tea, tea->val);
+ tea->freq = snd_tea575x_val_to_freq(tea, tea->val);
+}
+EXPORT_SYMBOL(snd_tea575x_set_freq);
+
+/*
+ * Linux Video interface
+ */
+
+static int vidioc_querycap(struct file *file, void *priv,
+ struct v4l2_capability *v)
+{
+ struct snd_tea575x *tea = video_drvdata(file);
+
+ strscpy(v->driver, tea->v4l2_dev->name, sizeof(v->driver));
+ strscpy(v->card, tea->card, sizeof(v->card));
+ strlcat(v->card, tea->tea5759 ? " TEA5759" : " TEA5757", sizeof(v->card));
+ strscpy(v->bus_info, tea->bus_info, sizeof(v->bus_info));
+ return 0;
+}
+
+int snd_tea575x_enum_freq_bands(struct snd_tea575x *tea,
+ struct v4l2_frequency_band *band)
+{
+ int index;
+
+ if (band->tuner != 0)
+ return -EINVAL;
+
+ switch (band->index) {
+ case 0:
+ if (tea->tea5759)
+ index = BAND_FM_JAPAN;
+ else
+ index = BAND_FM;
+ break;
+ case 1:
+ if (tea->has_am) {
+ index = BAND_AM;
+ break;
+ }
+ fallthrough;
+ default:
+ return -EINVAL;
+ }
+
+ *band = bands[index];
+ if (!tea->cannot_read_data)
+ band->capability |= V4L2_TUNER_CAP_HWSEEK_BOUNDED;
+
+ return 0;
+}
+EXPORT_SYMBOL(snd_tea575x_enum_freq_bands);
+
+static int vidioc_enum_freq_bands(struct file *file, void *priv,
+ struct v4l2_frequency_band *band)
+{
+ struct snd_tea575x *tea = video_drvdata(file);
+
+ return snd_tea575x_enum_freq_bands(tea, band);
+}
+
+int snd_tea575x_g_tuner(struct snd_tea575x *tea, struct v4l2_tuner *v)
+{
+ struct v4l2_frequency_band band_fm = { 0, };
+
+ if (v->index > 0)
+ return -EINVAL;
+
+ snd_tea575x_read(tea);
+ snd_tea575x_enum_freq_bands(tea, &band_fm);
+
+ memset(v, 0, sizeof(*v));
+ strscpy(v->name, tea->has_am ? "FM/AM" : "FM", sizeof(v->name));
+ v->type = V4L2_TUNER_RADIO;
+ v->capability = band_fm.capability;
+ v->rangelow = tea->has_am ? bands[BAND_AM].rangelow : band_fm.rangelow;
+ v->rangehigh = band_fm.rangehigh;
+ v->rxsubchans = tea->stereo ? V4L2_TUNER_SUB_STEREO : V4L2_TUNER_SUB_MONO;
+ v->audmode = (tea->val & TEA575X_BIT_MONO) ?
+ V4L2_TUNER_MODE_MONO : V4L2_TUNER_MODE_STEREO;
+ v->signal = tea->tuned ? 0xffff : 0;
+ return 0;
+}
+EXPORT_SYMBOL(snd_tea575x_g_tuner);
+
+static int vidioc_g_tuner(struct file *file, void *priv,
+ struct v4l2_tuner *v)
+{
+ struct snd_tea575x *tea = video_drvdata(file);
+
+ return snd_tea575x_g_tuner(tea, v);
+}
+
+static int vidioc_s_tuner(struct file *file, void *priv,
+ const struct v4l2_tuner *v)
+{
+ struct snd_tea575x *tea = video_drvdata(file);
+ u32 orig_val = tea->val;
+
+ if (v->index)
+ return -EINVAL;
+ tea->val &= ~TEA575X_BIT_MONO;
+ if (v->audmode == V4L2_TUNER_MODE_MONO)
+ tea->val |= TEA575X_BIT_MONO;
+ /* Only apply changes if currently tuning FM */
+ if (tea->band != BAND_AM && tea->val != orig_val)
+ snd_tea575x_set_freq(tea);
+
+ return 0;
+}
+
+static int vidioc_g_frequency(struct file *file, void *priv,
+ struct v4l2_frequency *f)
+{
+ struct snd_tea575x *tea = video_drvdata(file);
+
+ if (f->tuner != 0)
+ return -EINVAL;
+ f->type = V4L2_TUNER_RADIO;
+ f->frequency = tea->freq;
+ return 0;
+}
+
+static int vidioc_s_frequency(struct file *file, void *priv,
+ const struct v4l2_frequency *f)
+{
+ struct snd_tea575x *tea = video_drvdata(file);
+
+ if (f->tuner != 0 || f->type != V4L2_TUNER_RADIO)
+ return -EINVAL;
+
+ if (tea->has_am && f->frequency < (20000 * 16))
+ tea->band = BAND_AM;
+ else if (tea->tea5759)
+ tea->band = BAND_FM_JAPAN;
+ else
+ tea->band = BAND_FM;
+
+ tea->freq = clamp_t(u32, f->frequency, bands[tea->band].rangelow,
+ bands[tea->band].rangehigh);
+ snd_tea575x_set_freq(tea);
+ return 0;
+}
+
+int snd_tea575x_s_hw_freq_seek(struct file *file, struct snd_tea575x *tea,
+ const struct v4l2_hw_freq_seek *a)
+{
+ unsigned long timeout;
+ int i, spacing;
+
+ if (tea->cannot_read_data)
+ return -ENOTTY;
+ if (a->tuner || a->wrap_around)
+ return -EINVAL;
+
+ if (file->f_flags & O_NONBLOCK)
+ return -EWOULDBLOCK;
+
+ if (a->rangelow || a->rangehigh) {
+ for (i = 0; i < ARRAY_SIZE(bands); i++) {
+ if ((i == BAND_FM && tea->tea5759) ||
+ (i == BAND_FM_JAPAN && !tea->tea5759) ||
+ (i == BAND_AM && !tea->has_am))
+ continue;
+ if (bands[i].rangelow == a->rangelow &&
+ bands[i].rangehigh == a->rangehigh)
+ break;
+ }
+ if (i == ARRAY_SIZE(bands))
+ return -EINVAL; /* No matching band found */
+ if (i != tea->band) {
+ tea->band = i;
+ tea->freq = clamp(tea->freq, bands[i].rangelow,
+ bands[i].rangehigh);
+ snd_tea575x_set_freq(tea);
+ }
+ }
+
+ spacing = (tea->band == BAND_AM) ? 5 : 50; /* kHz */
+
+ /* clear the frequency, HW will fill it in */
+ tea->val &= ~TEA575X_BIT_FREQ_MASK;
+ tea->val |= TEA575X_BIT_SEARCH;
+ if (a->seek_upward)
+ tea->val |= TEA575X_BIT_UPDOWN;
+ else
+ tea->val &= ~TEA575X_BIT_UPDOWN;
+ snd_tea575x_write(tea, tea->val);
+ timeout = jiffies + msecs_to_jiffies(10000);
+ for (;;) {
+ if (time_after(jiffies, timeout))
+ break;
+ if (schedule_timeout_interruptible(msecs_to_jiffies(10))) {
+ /* some signal arrived, stop search */
+ tea->val &= ~TEA575X_BIT_SEARCH;
+ snd_tea575x_set_freq(tea);
+ return -ERESTARTSYS;
+ }
+ if (!(snd_tea575x_read(tea) & TEA575X_BIT_SEARCH)) {
+ u32 freq;
+
+ /* Found a frequency, wait until it can be read */
+ for (i = 0; i < 100; i++) {
+ msleep(10);
+ freq = snd_tea575x_get_freq(tea);
+ if (freq) /* available */
+ break;
+ }
+ if (freq == 0) /* shouldn't happen */
+ break;
+ /*
+ * if we moved by less than the spacing, or in the
+ * wrong direction, continue seeking
+ */
+ if (abs(tea->freq - freq) < 16 * spacing ||
+ (a->seek_upward && freq < tea->freq) ||
+ (!a->seek_upward && freq > tea->freq)) {
+ snd_tea575x_write(tea, tea->val);
+ continue;
+ }
+ tea->freq = freq;
+ tea->val &= ~TEA575X_BIT_SEARCH;
+ return 0;
+ }
+ }
+ tea->val &= ~TEA575X_BIT_SEARCH;
+ snd_tea575x_set_freq(tea);
+ return -ENODATA;
+}
+EXPORT_SYMBOL(snd_tea575x_s_hw_freq_seek);
+
+static int vidioc_s_hw_freq_seek(struct file *file, void *fh,
+ const struct v4l2_hw_freq_seek *a)
+{
+ struct snd_tea575x *tea = video_drvdata(file);
+
+ return snd_tea575x_s_hw_freq_seek(file, tea, a);
+}
+
+static int tea575x_s_ctrl(struct v4l2_ctrl *ctrl)
+{
+ struct snd_tea575x *tea = container_of(ctrl->handler, struct snd_tea575x, ctrl_handler);
+
+ switch (ctrl->id) {
+ case V4L2_CID_AUDIO_MUTE:
+ tea->mute = ctrl->val;
+ snd_tea575x_set_freq(tea);
+ return 0;
+ }
+
+ return -EINVAL;
+}
+
+static const struct v4l2_file_operations tea575x_fops = {
+ .unlocked_ioctl = video_ioctl2,
+ .open = v4l2_fh_open,
+ .release = v4l2_fh_release,
+ .poll = v4l2_ctrl_poll,
+};
+
+static const struct v4l2_ioctl_ops tea575x_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_s_hw_freq_seek = vidioc_s_hw_freq_seek,
+ .vidioc_enum_freq_bands = vidioc_enum_freq_bands,
+ .vidioc_log_status = v4l2_ctrl_log_status,
+ .vidioc_subscribe_event = v4l2_ctrl_subscribe_event,
+ .vidioc_unsubscribe_event = v4l2_event_unsubscribe,
+};
+
+static const struct video_device tea575x_radio = {
+ .ioctl_ops = &tea575x_ioctl_ops,
+ .release = video_device_release_empty,
+};
+
+static const struct v4l2_ctrl_ops tea575x_ctrl_ops = {
+ .s_ctrl = tea575x_s_ctrl,
+};
+
+
+int snd_tea575x_hw_init(struct snd_tea575x *tea)
+{
+ tea->mute = true;
+
+ /* Not all devices can or know how to read the data back.
+ Such devices can set cannot_read_data to true. */
+ if (!tea->cannot_read_data) {
+ snd_tea575x_write(tea, 0x55AA);
+ if (snd_tea575x_read(tea) != 0x55AA)
+ return -ENODEV;
+ }
+
+ tea->val = TEA575X_BIT_BAND_FM | TEA575X_BIT_SEARCH_5_28;
+ tea->freq = 90500 * 16; /* 90.5Mhz default */
+ snd_tea575x_set_freq(tea);
+
+ return 0;
+}
+EXPORT_SYMBOL(snd_tea575x_hw_init);
+
+int snd_tea575x_init(struct snd_tea575x *tea, struct module *owner)
+{
+ int retval = snd_tea575x_hw_init(tea);
+
+ if (retval)
+ return retval;
+
+ tea->vd = tea575x_radio;
+ video_set_drvdata(&tea->vd, tea);
+ mutex_init(&tea->mutex);
+ strscpy(tea->vd.name, tea->v4l2_dev->name, sizeof(tea->vd.name));
+ tea->vd.lock = &tea->mutex;
+ tea->vd.v4l2_dev = tea->v4l2_dev;
+ tea->vd.device_caps = V4L2_CAP_TUNER | V4L2_CAP_RADIO;
+ if (!tea->cannot_read_data)
+ tea->vd.device_caps |= V4L2_CAP_HW_FREQ_SEEK;
+ tea->fops = tea575x_fops;
+ tea->fops.owner = owner;
+ tea->vd.fops = &tea->fops;
+ /* disable hw_freq_seek if we can't use it */
+ if (tea->cannot_read_data)
+ v4l2_disable_ioctl(&tea->vd, VIDIOC_S_HW_FREQ_SEEK);
+
+ if (!tea->cannot_mute) {
+ tea->vd.ctrl_handler = &tea->ctrl_handler;
+ v4l2_ctrl_handler_init(&tea->ctrl_handler, 1);
+ v4l2_ctrl_new_std(&tea->ctrl_handler, &tea575x_ctrl_ops,
+ V4L2_CID_AUDIO_MUTE, 0, 1, 1, 1);
+ retval = tea->ctrl_handler.error;
+ if (retval) {
+ v4l2_err(tea->v4l2_dev, "can't initialize controls\n");
+ v4l2_ctrl_handler_free(&tea->ctrl_handler);
+ return retval;
+ }
+
+ if (tea->ext_init) {
+ retval = tea->ext_init(tea);
+ if (retval) {
+ v4l2_ctrl_handler_free(&tea->ctrl_handler);
+ return retval;
+ }
+ }
+
+ v4l2_ctrl_handler_setup(&tea->ctrl_handler);
+ }
+
+ retval = video_register_device(&tea->vd, VFL_TYPE_RADIO, tea->radio_nr);
+ if (retval) {
+ v4l2_err(tea->v4l2_dev, "can't register video device!\n");
+ v4l2_ctrl_handler_free(tea->vd.ctrl_handler);
+ return retval;
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL(snd_tea575x_init);
+
+void snd_tea575x_exit(struct snd_tea575x *tea)
+{
+ video_unregister_device(&tea->vd);
+ v4l2_ctrl_handler_free(tea->vd.ctrl_handler);
+}
+EXPORT_SYMBOL(snd_tea575x_exit);
diff --git a/drivers/media/radio/tef6862.c b/drivers/media/radio/tef6862.c
new file mode 100644
index 000000000..2596d7f4f
--- /dev/null
+++ b/drivers/media/radio/tef6862.c
@@ -0,0 +1,195 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * tef6862.c Philips TEF6862 Car Radio Enhanced Selectivity Tuner
+ * Copyright (c) 2009 Intel Corporation
+ */
+
+#include <linux/module.h>
+#include <linux/init.h>
+#include <linux/errno.h>
+#include <linux/kernel.h>
+#include <linux/interrupt.h>
+#include <linux/i2c.h>
+#include <linux/slab.h>
+#include <media/v4l2-ioctl.h>
+#include <media/v4l2-device.h>
+
+#define DRIVER_NAME "tef6862"
+
+#define FREQ_MUL 16000
+
+#define TEF6862_LO_FREQ (875U * FREQ_MUL / 10)
+#define TEF6862_HI_FREQ (108U * FREQ_MUL)
+
+/* Write mode sub addresses */
+#define WM_SUB_BANDWIDTH 0x0
+#define WM_SUB_PLLM 0x1
+#define WM_SUB_PLLL 0x2
+#define WM_SUB_DAA 0x3
+#define WM_SUB_AGC 0x4
+#define WM_SUB_BAND 0x5
+#define WM_SUB_CONTROL 0x6
+#define WM_SUB_LEVEL 0x7
+#define WM_SUB_IFCF 0x8
+#define WM_SUB_IFCAP 0x9
+#define WM_SUB_ACD 0xA
+#define WM_SUB_TEST 0xF
+
+/* Different modes of the MSA register */
+#define MSA_MODE_BUFFER 0x0
+#define MSA_MODE_PRESET 0x1
+#define MSA_MODE_SEARCH 0x2
+#define MSA_MODE_AF_UPDATE 0x3
+#define MSA_MODE_JUMP 0x4
+#define MSA_MODE_CHECK 0x5
+#define MSA_MODE_LOAD 0x6
+#define MSA_MODE_END 0x7
+#define MSA_MODE_SHIFT 5
+
+struct tef6862_state {
+ struct v4l2_subdev sd;
+ unsigned long freq;
+};
+
+static inline struct tef6862_state *to_state(struct v4l2_subdev *sd)
+{
+ return container_of(sd, struct tef6862_state, sd);
+}
+
+static u16 tef6862_sigstr(struct i2c_client *client)
+{
+ u8 buf[4];
+ int err = i2c_master_recv(client, buf, sizeof(buf));
+ if (err == sizeof(buf))
+ return buf[3] << 8;
+ return 0;
+}
+
+static int tef6862_g_tuner(struct v4l2_subdev *sd, struct v4l2_tuner *v)
+{
+ if (v->index > 0)
+ return -EINVAL;
+
+ /* only support FM for now */
+ strscpy(v->name, "FM", sizeof(v->name));
+ v->type = V4L2_TUNER_RADIO;
+ v->rangelow = TEF6862_LO_FREQ;
+ v->rangehigh = TEF6862_HI_FREQ;
+ v->rxsubchans = V4L2_TUNER_SUB_MONO;
+ v->capability = V4L2_TUNER_CAP_LOW;
+ v->audmode = V4L2_TUNER_MODE_STEREO;
+ v->signal = tef6862_sigstr(v4l2_get_subdevdata(sd));
+
+ return 0;
+}
+
+static int tef6862_s_tuner(struct v4l2_subdev *sd, const struct v4l2_tuner *v)
+{
+ return v->index ? -EINVAL : 0;
+}
+
+static int tef6862_s_frequency(struct v4l2_subdev *sd, const struct v4l2_frequency *f)
+{
+ struct tef6862_state *state = to_state(sd);
+ struct i2c_client *client = v4l2_get_subdevdata(sd);
+ unsigned freq = f->frequency;
+ u16 pll;
+ u8 i2cmsg[3];
+ int err;
+
+ if (f->tuner != 0)
+ return -EINVAL;
+
+ freq = clamp(freq, TEF6862_LO_FREQ, TEF6862_HI_FREQ);
+ pll = 1964 + ((freq - TEF6862_LO_FREQ) * 20) / FREQ_MUL;
+ i2cmsg[0] = (MSA_MODE_PRESET << MSA_MODE_SHIFT) | WM_SUB_PLLM;
+ i2cmsg[1] = (pll >> 8) & 0xff;
+ i2cmsg[2] = pll & 0xff;
+
+ err = i2c_master_send(client, i2cmsg, sizeof(i2cmsg));
+ if (err != sizeof(i2cmsg))
+ return err < 0 ? err : -EIO;
+
+ state->freq = freq;
+ return 0;
+}
+
+static int tef6862_g_frequency(struct v4l2_subdev *sd, struct v4l2_frequency *f)
+{
+ struct tef6862_state *state = to_state(sd);
+
+ if (f->tuner != 0)
+ return -EINVAL;
+ f->type = V4L2_TUNER_RADIO;
+ f->frequency = state->freq;
+ return 0;
+}
+
+static const struct v4l2_subdev_tuner_ops tef6862_tuner_ops = {
+ .g_tuner = tef6862_g_tuner,
+ .s_tuner = tef6862_s_tuner,
+ .s_frequency = tef6862_s_frequency,
+ .g_frequency = tef6862_g_frequency,
+};
+
+static const struct v4l2_subdev_ops tef6862_ops = {
+ .tuner = &tef6862_tuner_ops,
+};
+
+/*
+ * Generic i2c probe
+ * concerning the addresses: i2c wants 7 bit (without the r/w bit), so '>>1'
+ */
+
+static int tef6862_probe(struct i2c_client *client)
+{
+ struct tef6862_state *state;
+ struct v4l2_subdev *sd;
+
+ /* Check if the adapter supports the needed features */
+ if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_BYTE_DATA))
+ return -EIO;
+
+ v4l_info(client, "chip found @ 0x%02x (%s)\n",
+ client->addr << 1, client->adapter->name);
+
+ state = kzalloc_obj(struct tef6862_state);
+ if (state == NULL)
+ return -ENOMEM;
+ state->freq = TEF6862_LO_FREQ;
+
+ sd = &state->sd;
+ v4l2_i2c_subdev_init(sd, client, &tef6862_ops);
+
+ return 0;
+}
+
+static void tef6862_remove(struct i2c_client *client)
+{
+ struct v4l2_subdev *sd = i2c_get_clientdata(client);
+
+ v4l2_device_unregister_subdev(sd);
+ kfree(to_state(sd));
+}
+
+static const struct i2c_device_id tef6862_id[] = {
+ { .name = DRIVER_NAME },
+ { }
+};
+
+MODULE_DEVICE_TABLE(i2c, tef6862_id);
+
+static struct i2c_driver tef6862_driver = {
+ .driver = {
+ .name = DRIVER_NAME,
+ },
+ .probe = tef6862_probe,
+ .remove = tef6862_remove,
+ .id_table = tef6862_id,
+};
+
+module_i2c_driver(tef6862_driver);
+
+MODULE_DESCRIPTION("TEF6862 Car Radio Enhanced Selectivity Tuner");
+MODULE_AUTHOR("Mocean Laboratories");
+MODULE_LICENSE("GPL v2");