From a5f7a5bb3b7f28ba7e4fa246775b29a0e5537255 Mon Sep 17 00:00:00 2001 From: Jinke Han Date: Tue, 8 Sep 2026 15:37:42 +0800 Subject: x86/kprobes: Fix crash when probing CS CALL instructions When using eBPF to probe CS CALL instructions within a function, a crash can be triggered. The eBPF tool probes offset 257 of the __hrtimer_run_queues() function: <__hrtimer_run_queues+249>: nopl 0x0(%rax,%rax,1) <__hrtimer_run_queues+254>: mov %r14,%rdi <__hrtimer_run_queues+257>: cs call <__x86_indirect_thunk_r12> <__hrtimer_run_queues+263>: mov %eax,%r12d <__hrtimer_run_queues+266>: xchg %ax,%ax <__hrtimer_run_queues+268>: mov %r13,%rdi Which triggers this crash: BUG: unable to handle page fault for address: 00000000000f41c9 #PF: supervisor write access in kernel mode #PF: error_code(0x0002) - not-present page PGD 0 P4D 0 Oops: 0002 [#1] SMP NOPTI CPU: 1 PID: 0 Comm: swapper/1 Kdump: loaded Tainted: P RIP: 0010:__hrtimer_run_queues+0x106/0x230 Note that __hrtimer_run_queues+0x106 is __hrtimer_run_queues+262, which is at the 6th byte of the above CS CALL instruction. Since the CS CALL instruction occupies 6 bytes, the exception occurred in the middle of that call instruction. The root cause is that when using eBPF tools to probe in the middle of a function, a kprobe with INT3 is used as the underlying implementation. During single-step emulation of the original CALL instruction, int3_emulate_call() assumes that the probed CALL instruction is 5 bytes long. However, the actual CS-prefixed CALL instruction occupies 6 bytes, so it constructs an incorrect exception return address. When the CPU returns from the kprobe handler, the next instruction to be executed is at the address of the last byte of that CS CALL instruction. Coincidentally, starting from that address, the CPU fetches and decodes a completely different instruction, which ultimately triggers a kernel crash. Fix the issue by using the actual instruction length obtained from the instruction decoder when constructing the exception return address, rather than relying on the hardcoded CALL_INSN_SIZE macro. [ mingo: Refined the changelog ] Fixes: 6256e668b7af ("x86/kprobes: Use int3 instead of debug trap for single-step") Suggested-by: Masami Hiramatsu (Google) Signed-off-by: Jinke Han Signed-off-by: Ingo Molnar Reviewed-by: Masami Hiramatsu (Google) Acked-by: Yafang Shao Acked-by: Borislav Petkov Cc: Peter Zijlstra Link: https://patch.msgid.link/20260908073742.GA10517@didi-ThinkCentre-M920t-N000 --- drivers/media/radio/Kconfig | 488 ++++++ drivers/media/radio/Makefile | 39 + drivers/media/radio/dsbr100.c | 420 +++++ drivers/media/radio/lm7000.h | 44 + drivers/media/radio/radio-aimslab.c | 195 +++ drivers/media/radio/radio-aztech.c | 161 ++ drivers/media/radio/radio-cadet.c | 679 ++++++++ drivers/media/radio/radio-gemtek.c | 340 ++++ drivers/media/radio/radio-isa.c | 377 +++++ drivers/media/radio/radio-isa.h | 101 ++ drivers/media/radio/radio-keene.c | 405 +++++ drivers/media/radio/radio-ma901.c | 456 ++++++ drivers/media/radio/radio-maxiradio.c | 203 +++ drivers/media/radio/radio-miropcm20.c | 513 ++++++ drivers/media/radio/radio-mr800.c | 599 +++++++ drivers/media/radio/radio-raremono.c | 389 +++++ drivers/media/radio/radio-rtrack2.c | 142 ++ drivers/media/radio/radio-sf16fmi.c | 382 +++++ drivers/media/radio/radio-sf16fmr2.c | 345 ++++ drivers/media/radio/radio-shark.c | 429 +++++ drivers/media/radio/radio-shark2.c | 395 +++++ drivers/media/radio/radio-si476x.c | 1523 ++++++++++++++++++ drivers/media/radio/radio-tea5764.c | 529 +++++++ drivers/media/radio/radio-tea5777.c | 587 +++++++ drivers/media/radio/radio-tea5777.h | 76 + drivers/media/radio/radio-terratec.c | 168 ++ drivers/media/radio/radio-timb.c | 176 +++ drivers/media/radio/radio-trust.c | 243 +++ drivers/media/radio/radio-typhoon.c | 181 +++ drivers/media/radio/radio-zoltrix.c | 249 +++ drivers/media/radio/saa7706h.c | 417 +++++ drivers/media/radio/si470x/Kconfig | 52 + drivers/media/radio/si470x/Makefile | 8 + drivers/media/radio/si470x/radio-si470x-common.c | 794 ++++++++++ drivers/media/radio/si470x/radio-si470x-i2c.c | 545 +++++++ drivers/media/radio/si470x/radio-si470x-usb.c | 849 ++++++++++ drivers/media/radio/si470x/radio-si470x.h | 220 +++ drivers/media/radio/si4713/Kconfig | 41 + drivers/media/radio/si4713/Makefile | 8 + drivers/media/radio/si4713/radio-platform-si4713.c | 211 +++ drivers/media/radio/si4713/radio-usb-si4713.c | 522 ++++++ drivers/media/radio/si4713/si4713.c | 1665 ++++++++++++++++++++ drivers/media/radio/si4713/si4713.h | 254 +++ drivers/media/radio/tea575x.c | 577 +++++++ drivers/media/radio/tef6862.c | 195 +++ 45 files changed, 17192 insertions(+) create mode 100644 drivers/media/radio/Kconfig create mode 100644 drivers/media/radio/Makefile create mode 100644 drivers/media/radio/dsbr100.c create mode 100644 drivers/media/radio/lm7000.h create mode 100644 drivers/media/radio/radio-aimslab.c create mode 100644 drivers/media/radio/radio-aztech.c create mode 100644 drivers/media/radio/radio-cadet.c create mode 100644 drivers/media/radio/radio-gemtek.c create mode 100644 drivers/media/radio/radio-isa.c create mode 100644 drivers/media/radio/radio-isa.h create mode 100644 drivers/media/radio/radio-keene.c create mode 100644 drivers/media/radio/radio-ma901.c create mode 100644 drivers/media/radio/radio-maxiradio.c create mode 100644 drivers/media/radio/radio-miropcm20.c create mode 100644 drivers/media/radio/radio-mr800.c create mode 100644 drivers/media/radio/radio-raremono.c create mode 100644 drivers/media/radio/radio-rtrack2.c create mode 100644 drivers/media/radio/radio-sf16fmi.c create mode 100644 drivers/media/radio/radio-sf16fmr2.c create mode 100644 drivers/media/radio/radio-shark.c create mode 100644 drivers/media/radio/radio-shark2.c create mode 100644 drivers/media/radio/radio-si476x.c create mode 100644 drivers/media/radio/radio-tea5764.c create mode 100644 drivers/media/radio/radio-tea5777.c create mode 100644 drivers/media/radio/radio-tea5777.h create mode 100644 drivers/media/radio/radio-terratec.c create mode 100644 drivers/media/radio/radio-timb.c create mode 100644 drivers/media/radio/radio-trust.c create mode 100644 drivers/media/radio/radio-typhoon.c create mode 100644 drivers/media/radio/radio-zoltrix.c create mode 100644 drivers/media/radio/saa7706h.c create mode 100644 drivers/media/radio/si470x/Kconfig create mode 100644 drivers/media/radio/si470x/Makefile create mode 100644 drivers/media/radio/si470x/radio-si470x-common.c create mode 100644 drivers/media/radio/si470x/radio-si470x-i2c.c create mode 100644 drivers/media/radio/si470x/radio-si470x-usb.c create mode 100644 drivers/media/radio/si470x/radio-si470x.h create mode 100644 drivers/media/radio/si4713/Kconfig create mode 100644 drivers/media/radio/si4713/Makefile create mode 100644 drivers/media/radio/si4713/radio-platform-si4713.c create mode 100644 drivers/media/radio/si4713/radio-usb-si4713.c create mode 100644 drivers/media/radio/si4713/si4713.c create mode 100644 drivers/media/radio/si4713/si4713.h create mode 100644 drivers/media/radio/tea575x.c create mode 100644 drivers/media/radio/tef6862.c (limited to 'drivers/media/radio') 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 + . + + 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 + . + + 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 + . + + 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 + . + + 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 + . + + 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 +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * Version Information + */ +MODULE_AUTHOR("Markus Demleitner "); +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 + * 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 + * Converted to V4L2 API by Mauro Carvalho Chehab + * Converted to new API by Alan Cox + * Various bugfixes and enhancements by Russell Kroll + * + * 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 /* Modules */ +#include /* Initdata */ +#include /* request_region */ +#include /* msleep */ +#include /* kernel radio structs */ +#include /* outb, outb_p */ +#include +#include +#include +#include +#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 + * Converted to V4L2 API by Mauro Carvalho Chehab + * Adapted to support the Video for Linux API by + * Russell Kroll . 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 /* Modules */ +#include /* Initdata */ +#include /* request_region */ +#include /* udelay */ +#include /* kernel radio structs */ +#include /* outb, outb_p */ +#include +#include +#include +#include +#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 + * 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 + * Added ISAPnP detection for Linux 2.3/2.4 + * + * 2001-01-10 Russell Kroll + * Removed dead CONFIG_RADIO_CADET_PORT code + * PnP detection on load is now default (no args necessary) + * + * 2002-01-17 Adam Belay + * Updated to latest pnp code + * + * 2003-01-31 Alan Cox + * Cleaned up locking, delay code, general odds and ends + * + * 2006-07-30 Hans J. Koch + * Changed API to V4L2 + */ + +#include /* Modules */ +#include /* Initdata */ +#include /* request_region */ +#include /* udelay */ +#include /* V4L2 API defs */ +#include +#include +#include +#include /* outb, outb_p */ +#include +#include +#include +#include +#include + +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 + * + * 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 + * Various bugfixes and enhancements by Russell Kroll + * + * Converted to the radio-isa framework by Hans Verkuil + * Converted to V4L2 API by Mauro Carvalho Chehab + * + * 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 /* Modules */ +#include /* Initdata */ +#include /* request_region */ +#include /* udelay */ +#include /* kernel radio structs */ +#include +#include /* outb, outb_p */ +#include +#include +#include +#include +#include "radio-isa.h" + +/* + * Module info. + */ + +MODULE_AUTHOR("Jonas Munsin, Pekka Seppänen "); +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<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 = >->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 ? >->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 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 + */ + +#ifndef _RADIO_ISA_H_ +#define _RADIO_ISA_H_ + +#include +#include +#include +#include +#include + +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 + */ + +/* kernel includes */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* driver and module definitions */ +MODULE_AUTHOR("Hans Verkuil "); +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 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DRIVER_AUTHOR "Alexey Klimov " +#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 + * + * 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 + * + * 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 : + * - Conversion to V4L2 API + * - Uses video_ioctl2 for parsing and to add debug support + */ + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 + * 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 + * + * Many thanks to Fred Seidel , the + * designer of the RDS decoder hardware. With his help + * I was able to code this driver. + * Thanks also to Norberto Pellicci, Dominic Mounteney + * 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 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 + */ + +/* + * 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 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 and + * David Ellingsworth + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* driver and module definitions */ +#define DRIVER_AUTHOR "Alexey Klimov " +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * '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 and finished by Hans Verkuil + * . + * + * 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 "); +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 + * Various bugfixes and enhancements by Russell Kroll + * + * Converted to the radio-isa framework by Hans Verkuil + * Converted to V4L2 API by Mauro Carvalho Chehab + * + * Fully tested with actual hardware and the v4l2-compliance tool. + */ + +#include /* Modules */ +#include /* Initdata */ +#include /* request_region */ +#include /* udelay */ +#include /* kernel radio structs */ +#include +#include /* outb, outb_p */ +#include +#include +#include +#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 + * Made working and cleaned up functions + * Support for ISAPnP by Ladislav Michl + * + * 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 + */ + +#include /* __setup */ +#include /* Modules */ +#include /* Initdata */ +#include /* request_region */ +#include /* udelay */ +#include +#include +#include /* kernel radio structs */ +#include /* outb, outb_p */ +#include +#include +#include +#include +#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 +#include /* Modules */ +#include /* Initdata */ +#include +#include /* request_region */ +#include /* outb, outb_p */ +#include +#include +#include + +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 + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include + +#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 "); +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 + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#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 "); +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 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#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 "); +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 + * + * History: + * 2008-12-06 Fabio Belavenuto + * initial code + * + * TODO: + * add platform_data support for IRQs platform dependencies + * add RDS support + */ +#include +#include +#include +#include /* Initdata */ +#include /* kernel radio structs */ +#include /* I2C */ +#include +#include +#include +#include +#include + +#define DRIVER_VERSION "0.0.2" + +#define DRIVER_AUTHOR "Fabio Belavenuto " +#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 + * + * Based on the ALSA driver for TEA5757/5759 Philips AM/FM radio tuner chips: + * + * Copyright (c) 2004 Jaroslav Kysela + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "radio-tea5777.h" + +MODULE_AUTHOR("Hans de Goede "); +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 + * + * Based on the ALSA driver for TEA5757/5759 Philips AM/FM radio tuner chips: + * + * Copyright (c) 2004 Jaroslav Kysela + * Copyright (c) 2012 Hans de Goede + */ + +#include +#include +#include +#include + +#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 + * Converted to V4L2 API by Mauro Carvalho Chehab + */ + +#include /* Modules */ +#include /* Initdata */ +#include /* request_region */ +#include /* kernel radio structs */ +#include +#include /* outb, outb_p */ +#include +#include +#include +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 "); +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 + * + * Based on radio-aztech.c. Original notes: + * + * Adapted to support the Video for Linux API by + * Russell Kroll . 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 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#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 + * + * 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 + */ + +#include /* Modules */ +#include /* Initdata */ +#include /* request_region */ +#include /* kernel radio structs */ +#include /* outb, outb_p */ +#include +#include +#include +#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 + * + * 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 + * + * Converted to the radio-isa framework by Hans Verkuil + * + * 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 /* Modules */ +#include /* Initdata */ +#include /* request_region */ +#include /* udelay, msleep */ +#include /* kernel radio structs */ +#include +#include /* outb, outb_p */ +#include +#include +#include +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 + * Copyright (c) 2012 Hans de Goede + */ + + +/* + * History: + * 2008-01-12 Tobias Lorenz + * Version 1.0.0 + * - First working version + * 2008-01-13 Tobias Lorenz + * 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 + * 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 + * 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 + * 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 + * 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 + * Oliver Neukum + * 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 + * Oliver Neukum + * Version 1.0.7 + * - usb autosuspend support + * - unplugging fixed + * 2008-05-07 Tobias Lorenz + * 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 + * - add support for KWorld USB FM Radio FM700 + * - blacklisted KWorld radio in hid-core.c and hid-ids.h + * 2008-12-03 Mark Lord + * - add support for DealExtreme USB Radio + * 2009-01-31 Bob Ross + * - correction of stereo detection/setting + * - correction of signal strength indicator scaling + * 2009-01-31 Rick Bronson + * Tobias Lorenz + * - add LED status output + * - get HW/SW version from scratchpad + * 2009-06-16 Edouard Lafargue + * 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 + */ + + +/* driver definitions */ +#define DRIVER_AUTHOR "Joonyoung Shim " +#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 +#include +#include +#include +#include + +#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 + */ + + +/* + * ToDo: + * - add firmware download/update support + */ + + +/* driver definitions */ +#define DRIVER_AUTHOR "Tobias Lorenz " +#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 +#include +#include + +#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 + */ + + +/* driver definitions */ +#define DRIVER_NAME "radio-si470x" + + +/* kernel includes */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + + +/************************************************************************** + * 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 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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 "); +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 +#include +#include +#include +#include +#include +#include +#include +/* V4l includes */ +#include +#include +#include +#include +#include +#include + +#include "si4713.h" + +/* driver and module definitions */ +MODULE_AUTHOR("Dinesh Ram "); +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 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 "); +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 + * + * 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 +#include +#include +#include +#include +#include + +#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 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +MODULE_AUTHOR("Jaroslav Kysela "); +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 +#include +#include +#include +#include +#include +#include +#include +#include + +#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"); -- cgit v1.3.1