Message ID | 20200803020921.64151-4-santiagohssl@gmail.com (mailing list archive) |
---|---|
State | New, archived |
Headers | show |
Series | KT0913 FM/AM driver | expand |
On 03/08/2020 04:09, Santiago Hormazabal wrote: > This chip requires almost no support components and can used over I2C. > The driver uses the I2C bus and exposes the controls as a V4L2 radio. > Tested with a module that contains this chip (from SZZSJDZ.com, > part number ZJ-801B, even tho the company seems defunct now), and an H2+ > AllWinner SoC running a kernel built off 8f2a4a9 of the media_tree. > > Signed-off-by: Santiago Hormazabal <santiagohssl@gmail.com> > --- It's good practice to list the changes you made for a v2 either in the cover letter or here (below ---). I have a preference myself for the cover letter, but either is fine. > drivers/media/radio/Kconfig | 10 + > drivers/media/radio/Makefile | 1 + > drivers/media/radio/radio-kt0913.c | 1196 ++++++++++++++++++++++++++++ > 3 files changed, 1207 insertions(+) > create mode 100644 drivers/media/radio/radio-kt0913.c > > diff --git a/drivers/media/radio/Kconfig b/drivers/media/radio/Kconfig > index d29e29645e04..ac9053a95f3a 100644 > --- a/drivers/media/radio/Kconfig > +++ b/drivers/media/radio/Kconfig > @@ -226,6 +226,16 @@ config RADIO_WL1273 > # TI's ST based wl128x FM radio > source "drivers/media/radio/wl128x/Kconfig" > > +config RADIO_KT0913 > + tristate "KT0913 I2C FM/AM radio support" > + depends on I2C && VIDEO_V4L2 > + help > + Say Y here if you want to use the KT0913 FM/AM chip. > + This is a low cost chip that uses the I2C bus. > + > + To compile this driver as a module, choose M here: the > + module will be called radio-kt0913. > + > # > # ISA drivers configuration > # > diff --git a/drivers/media/radio/Makefile b/drivers/media/radio/Makefile > index 53c7ae135460..a314121f7771 100644 > --- a/drivers/media/radio/Makefile > +++ b/drivers/media/radio/Makefile > @@ -34,5 +34,6 @@ obj-$(CONFIG_RADIO_WL1273) += radio-wl1273.o > obj-$(CONFIG_RADIO_WL128X) += wl128x/ > obj-$(CONFIG_RADIO_TEA575X) += tea575x.o > obj-$(CONFIG_USB_RAREMONO) += radio-raremono.o > +obj-$(CONFIG_RADIO_KT0913) += radio-kt0913.o > > shark2-objs := radio-shark2.o radio-tea5777.o > diff --git a/drivers/media/radio/radio-kt0913.c b/drivers/media/radio/radio-kt0913.c > new file mode 100644 > index 000000000000..e8c1ff882779 > --- /dev/null > +++ b/drivers/media/radio/radio-kt0913.c > @@ -0,0 +1,1196 @@ > +// SPDX-License-Identifier: GPL-2.0-only > +/* > + * drivers/media/radio/radio-kt0913.c > + * > + * Driver for the KT0913 radio chip from KTMicro. > + * This driver provides a v4l2 interface to the tuner, using the I2C > + * protocol to communicate with the chip. > + * It exposes two bands, one for AM and another for FM. If the "campus > + * band" feature needs to be enabled, set the corresponding module parameter > + * to 1. > + * Reference Clock and Audio DAC anti-pop configurations should be > + * set via a device tree node. Defaults will be used otherwise. > + * > + * Audio output should be routed to a speaker or an audio capture > + * device. > + * > + * Based on radio-tea5764 by Fabio Belavenuto <belavenuto@gmail.com> > + * > + * Copyright (c) 2020 Santiago Hormazabal <santiagohssl@gmail.com> > + * > + * TODO: > + * use rd and wr support for the regmap instead of volatile regs. > + * add support for the hardware-assisted frequency seek. > + * export FM SNR and AM/FM AFC deviation values as RO controls. > + */ > + > +#include <linux/module.h> > +#include <linux/i2c.h> > +#include <linux/err.h> > +#include <linux/pm_runtime.h> > +#include <linux/of.h> > +#include <linux/math64.h> > +#include <linux/regmap.h> > +#include <media/v4l2-ctrls.h> > +#include <media/v4l2-device.h> > +#include <media/v4l2-ioctl.h> > +#include <media/v4l2-event.h> > + > + /* ************************************************************************* */ > + > + /* registers of the kt0913 */ > +#define KT0913_REG_CHIP_ID 0x01 > +#define KT0913_REG_SEEK 0x02 > +#define KT0913_REG_TUNE 0x03 > +#define KT0913_REG_VOLUME 0x04 > +#define KT0913_REG_DSPCFGA 0x05 > +#define KT0913_REG_LOCFGA 0x0A > +#define KT0913_REG_LOCFGC 0x0C > +#define KT0913_REG_RXCFG 0x0F > +#define KT0913_REG_STATUSA 0x12 > +#define KT0913_REG_STATUSB 0x13 > +#define KT0913_REG_STATUSC 0x14 > +#define KT0913_REG_AMSYSCFG 0x16 > +#define KT0913_REG_AMCHAN 0x17 > +#define KT0913_REG_AMCALI 0x18 > +#define KT0913_REG_GPIOCFG 0x1D > +#define KT0913_REG_AMDSP 0x22 > +#define KT0913_REG_AMSTATUSA 0x24 > +#define KT0913_REG_AMSTATUSB 0x25 > +#define KT0913_REG_SOFTMUTE 0x2E > +#define KT0913_REG_AMCFG 0x33 > +#define KT0913_REG_AMCFG2 0x34 > +#define KT0913_REG_AFC 0x3C Hmm, this is still uppercase. > + > +/* register symbols masks, values and shift count */ > +#define KT0913_TUNE_FMTUNE_MASK 0x8000 /* FM Tune enable */ > +#define KT0913_TUNE_FMTUNE_ON 0x8000 /* FM Tune enabled */ > +#define KT0913_TUNE_FMTUNE_OFF 0x0000 /* FM Tune disabled */ > +#define KT0913_TUNE_FMCHAN_MASK 0x0FFF /* frequency in kHz / 50kHz */ > + > +#define KT0913_VOLUME_DMUTE_MASK 0x2000 > +#define KT0913_VOLUME_DMUTE_ON 0x0000 > +#define KT0913_VOLUME_DMUTE_OFF 0x2000 > +#define KT0913_VOLUME_DE_MASK 0x0800 /* de-emphasis time constant */ > +#define KT0913_VOLUME_DE_75US 0x0000 /* 75us */ > +#define KT0913_VOLUME_DE_50US 0x0800 /* 50us */ > +#define KT0913_VOLUME_POP_MASK 0x30 /* audio dac anti-pop config */ > +#define KT0913_VOLUME_POP_SHIFT 4 > + > +#define KT0913_DSPCFGA_MONO_MASK 0x8000 /* mono select (0=stereo, 1=mono) */ > +#define KT0913_DSPCFGA_MONO_ON 0x8000 /* mono */ > +#define KT0913_DSPCFGA_MONO_OFF 0x0000 /* stereo */ > + > +#define KT0913_LOCFG_CAMPUSBAND_EN_MASK 0x0008 /* campus band fm enable */ > +#define KT0913_LOCFG_CAMPUSBAND_EN_ON 0x0008 /* FM range 64-110MHz */ > +#define KT0913_LOCFG_CAMPUSBAND_EN_OFF 0x0000 /* FM range 32-110MHz */ > + > +#define KT0913_RXCFGA_STDBY_MASK 0x1000 /* standby mode enable */ > +#define KT0913_RXCFGA_STDBY_ON 0x1000 /* standby mode enabled */ > +#define KT0913_RXCFGA_STDBY_OFF 0x0000 /* standby mode disabled */ > +#define KT0913_RXCFGA_VOLUME_MASK 0x001F /* volume control */ > + > +#define KT0913_STATUSA_XTAL_OK 0x8000 /* crystal ready indicator */ > +#define KT0913_STATUSA_STC 0x4000 /* seek/tune complete */ > + > +#define KT0913_STATUSA_PLL_LOCK_MASK 0x800 /* system pll ready indicator */ > +#define KT0913_STATUSA_PLL_LOCK_LOCKED 0x800 /* system pll ready */ > +#define KT0913_STATUSA_PLL_LOCK_UNLOCKED 0x000 /* not ready */ > +#define KT0913_STATUSA_LO_LOCK 0x400 /* LO synthesizer ready indicator */ > +#define KT0913_STATUSA_ST_MASK 0x300 /* stereo indicator (0x300=stereo, otherwise mono) */ > +#define KT0913_STATUSA_ST_STEREO 0x300 /* stereo */ > +#define KT0913_STATUSA_FMRSSI_MASK 0xF8 /* FM RSSI (-100dBm + FMRSSI*3dBm) */ > +#define KT0913_STATUSA_FMRSSI_SHIFT 3 > + > +#define KT0913_STATUSC_PWSTATUS 0x8000 /* power status indicator */ > +#define KT0913_STATUSC_CHIPRDY 0x2000 /* chip ready indicator */ > +#define KT0913_STATUSC_FMSNR 0x1FC0 /* FM SNR (unknown units) */ > + > +#define KT0913_AMCHAN_AMTUNE_MASK 0x8000 /* AM tune enable */ > +#define KT0913_AMCHAN_AMTUNE_ON 0x8000 /* AM tune enabled */ > +#define KT0913_AMCHAN_AMTUNE_OFF 0x0000 /* AM tune disabled */ > +#define KT0913_AMCHAN_AMCHAN_MASK 0x7FF /* am channel in kHz */ > + > +#define KT0913_AMSYSCFG_AM_FM_MASK 0x8000 /* am/fm mode control */ > +#define KT0913_AMSYSCFG_AM_FM_AM 0x8000 /* am mode */ > +#define KT0913_AMSYSCFG_AM_FM_FM 0x0000 /* fm mode (default) */ > +#define KT0913_AMSYSCFG_REFCLK_MASK 0x0F00 /* reference clock selection */ > +#define KT0913_AMSYSCFG_REFCLK_SHIFT 8 > +#define KT0913_AMSYSCFG_AU_GAIN_MASK 0x00C0 /* audio gain selection */ > +#define KT0913_AMSYSCFG_AU_GAIN_6DB 0x0040 /* 6dB audio gain */ > +#define KT0913_AMSYSCFG_AU_GAIN_3DB 0x0000 /* 3dB audio gain (default) */ > +#define KT0913_AMSYSCFG_AU_GAIN_0DB 0x00C0 /* 0dB audio gain */ > +#define KT0913_AMSYSCFG_AU_GAIN_MIN_3DB 0x0080 /* -3dB audio gain */ > + > +#define KT0913_AMSTATUSA_AMRSSI_MASK 0x1F00 /* am channel rssi */ > +#define KT0913_AMSTATUSA_AMRSSI_SHIFT 8 > + > +/* constants */ > +#define KT0913_CHIP_ID 0x544B /* ASCII of 'KT' */ > + > +#define V4L2_KHZ_FREQ_MUL 16U /* v4l2 uses 16x the kHz value as their freq */ > +#define KT0913_FMCHAN_MUL 50U /* kt0913 uses freqs with a 50kHz multiplier */ > +#define KT0913_FM_RANGE_LOW_NO_CAMPUS 64000U /* 64MHz lower bound for FM */ > +#define KT0913_FM_RANGE_LOW_CAMPUS 32000U /* 32MHz lower bound for campus FM */ > +#define KT0913_FM_RANGE_HIGH 110000U /* 110MHz upper bound for FM */ > +#define KT0913_AM_RANGE_LOW 500U /* 500kHz lower bound for AM */ > +#define KT0913_AM_RANGE_HIGH 1710U /* 1710kHz upper bound for AM */ > + > +#define KT0913_FM_AM_DRIVER_NAME "kt0913-fm-am" Just name it "kt0913". Does it just support the 0913 or other variants as well? I wonder if it should perhaps be named "kt091x". Up to you. > + > +/* ************************************************************************* */ > + > +/* v4l2 device number to use. -1 will assign the next free one */ > +static int kt0913_v4l2_radio_nr = -1; > +/* use the extended range of FM down to 32MHz. disabled by default */ > +static int kt0913_use_campus_band; > + > +/* ************************************************************************* */ > + > +/* kt0913 status struct */ > +struct kt0913_device { > + struct v4l2_device v4l2_dev; /* main v4l2 struct */ > + struct i2c_client *client; /* I2C client */ > + struct video_device vdev; /* vide_device struct */ > + struct v4l2_ctrl_handler ctrl_handler; /* ctrl_handler struct */ > + > + /* V4L2 Controls */ > + struct v4l2_ctrl *ctrl_pll_lock; /* PLL lock */ > + struct v4l2_ctrl *ctrl_volume; /* Overall volume */ > + struct v4l2_ctrl *ctrl_au_gain; /* Audio Gain */ > + struct v4l2_ctrl *ctrl_mute; /* Master mute */ > + struct v4l2_ctrl *ctrl_deemp; /* Deemphasis */ > + > + /* current operation band (fm, fm_campus, am) */ > + unsigned int band; > + > + /* audio dac anti-pop setting: > + * 0 -> 100uF (default) > + * 1 -> 60uF > + * 2 -> 20uF > + * 3 -> 10uF > + */ > + unsigned int audio_anti_pop; > + > + /* > + * reference clock selection: > + * 0 -> 32.768kHz (default) > + * 1 -> 6.5MHz > + * 2 -> 7.6MHz > + * 3 -> 12MHz > + * 4 -> 13MHz > + * 5 -> 15.2MHz > + * 6 -> 19.2MHz > + * 7 -> 24MHz > + * 8 -> 26MHz > + * 9 -> 38kHz > + */ > + unsigned int refclock_val; > + > + /* Regmap */ > + struct regmap *regmap; > + > + /* For core assisted locking */ > + struct mutex mutex; > +}; > + > +/* ************************************************************************* */ > + > +/* Regmap settings */ > +static const struct regmap_range kt0913_regmap_all_registers_range[] = { > + regmap_reg_range(0x01, 0x05), > + regmap_reg_range(0x0A, 0x0A), > + regmap_reg_range(0x0C, 0x0C), > + regmap_reg_range(0x0F, 0x0F), > + regmap_reg_range(0x12, 0x14), > + regmap_reg_range(0x16, 0x18), > + regmap_reg_range(0x1D, 0x1D), > + regmap_reg_range(0x22, 0x22), > + regmap_reg_range(0x24, 0x25), > + regmap_reg_range(0x2E, 0x2F), > + regmap_reg_range(0x30, 0x34), > + regmap_reg_range(0x3A, 0x3A), > + regmap_reg_range(0x3C, 0x3C), > +}; > + > +static const struct regmap_access_table kt0913_all_registers_access_table = { > + .yes_ranges = kt0913_regmap_all_registers_range, > + .n_yes_ranges = ARRAY_SIZE(kt0913_regmap_all_registers_range), > +}; > + > +static const struct reg_sequence kt0913_init_regs_to_defaults[] = { > + /* Standby disabled, volume 0dB */ > + { KT0913_REG_RXCFG, 0x881F }, > + /* FM Channel spacing = 50kHz, Right & Left unmuted */ > + { KT0913_REG_SEEK, 0x000B }, > + /* Stereo, High Stereo/Mono blend level, blend disabled */ > + { KT0913_REG_DSPCFGA, 0x1000 }, > + /* FM AFC Enabled */ > + { KT0913_REG_LOCFGA, 0x0100 }, > + /* Campus band disabled by default */ > + { KT0913_REG_LOCFGC, 0x0024 }, > + /* > + * FM mode, internal defined bands, clock from XT, 32.768kHz > + * 3dB audio gain, AM AFC Enabled > + */ > + { KT0913_REG_AMSYSCFG, 0x0002 }, > + /* Default AM freq = 504kHz */ > + { KT0913_REG_AMCHAN, 0x01F8}, > + /* VOL and CH GPIOs set to HiZ */ > + { KT0913_REG_GPIOCFG, 0x0000 }, > + /* AM Channel bandwidth = 6kHz, non-differential output */ > + { KT0913_REG_AMDSP, 0xAFC4 }, > + /* > + * softmute is disabled on AM and FM, but set the defaults: > + * strong softmute attn., slow softmute attack/recover, > + * lowest AM softumte start level, almost the minimum > + * softmute target volume, RSSI mode for softmute, lowest > + * FM softmute start level > + */ > + { KT0913_REG_SOFTMUTE, 0x0010 }, > + /* 1kHz for AM channel space, working mode A for the keys */ > + { KT0913_REG_AMCFG, 0x1401 }, > + /* TIME1 = shortest, TIME2 = fastest */ > + { KT0913_REG_AMCFG, 0x4050 }, > + /* set 86MHz as the default frequency, and tune it */ > + { KT0913_REG_TUNE, 0x86B8 }, > + /* > + * FM&AM Softmute disabled, Mute disabled, 75us deemp., > + * no bass boost, 100uF anti pop cap > + */ > + { KT0913_REG_VOLUME, 0xE080 }, > +}; > + > +static const struct regmap_config kt0913_regmap_config = { > + .reg_bits = 8, > + .val_bits = 16, > + .max_register = KT0913_REG_AFC, > + .volatile_table = &kt0913_all_registers_access_table, > + .cache_type = REGCACHE_RBTREE, > + .val_format_endian = REGMAP_ENDIAN_BIG, > +}; > + > +/* ************************************************************************* */ > + > +/* bands where the kt0913 operates */ > +enum { BAND_FM, BAND_FM_CAMUS, BAND_AM }; This is still CAMUS. I wonder if you reposted v1 instead of v2. I stop reviewing because this doesn't seem right. Regards, Hans > + > +static const struct v4l2_frequency_band kt0913_bands[] = { > + { > + /* BAND_FM */ > + .type = V4L2_TUNER_RADIO, > + .index = 0, /* index provided to v4l2 */ > + .capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO | > + V4L2_TUNER_CAP_FREQ_BANDS, > + .rangelow = KT0913_FM_RANGE_LOW_NO_CAMPUS * V4L2_KHZ_FREQ_MUL, > + .rangehigh = KT0913_FM_RANGE_HIGH * V4L2_KHZ_FREQ_MUL, > + .modulation = V4L2_BAND_MODULATION_FM, > + }, > + { > + /* BAND_FM_CAMUS */ > + .type = V4L2_TUNER_RADIO, > + .index = 0, /* index provided to v4l2 */ > + .capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO | > + V4L2_TUNER_CAP_FREQ_BANDS, > + .rangelow = KT0913_FM_RANGE_LOW_CAMPUS * V4L2_KHZ_FREQ_MUL, > + .rangehigh = KT0913_FM_RANGE_HIGH * V4L2_KHZ_FREQ_MUL, > + .modulation = V4L2_BAND_MODULATION_FM, > + }, > + { > + /* BAND_AM */ > + .type = V4L2_TUNER_RADIO, > + .index = 1, /* index provided to v4l2 */ > + .capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_FREQ_BANDS, > + .rangelow = KT0913_AM_RANGE_LOW * V4L2_KHZ_FREQ_MUL, > + .rangehigh = KT0913_AM_RANGE_HIGH * V4L2_KHZ_FREQ_MUL, > + .modulation = V4L2_BAND_MODULATION_AM, > + }, > +}; > + > +/* ************************************************************************* */ > + > +static inline struct kt0913_device * > +v4l2_device_to_device(struct v4l2_device *v4l2_dev) > +{ > + return container_of(v4l2_dev, > + struct kt0913_device, v4l2_dev); > +} > + > +static inline struct kt0913_device * > +v4l2_ctrl_to_device(struct v4l2_ctrl *ctrl_handler) > +{ > + return container_of(ctrl_handler->handler, > + struct kt0913_device, ctrl_handler); > +} > + > +/* ************************************************************************* */ > + > +static inline u32 khz_to_v4l2_freq(unsigned int freq) > +{ > + return freq * V4L2_KHZ_FREQ_MUL; > +} > + > +static inline unsigned int v4l2_freq_to_khz(u32 v4l2_freq) > +{ > + return v4l2_freq / V4L2_KHZ_FREQ_MUL; > +} > + > +/* ************************************************************************* */ > + > +static int __kt0913_get_fm_frequency(struct kt0913_device *radio, > + unsigned int *frequency) > +{ > + unsigned int tune_reg; > + int ret = regmap_read(radio->regmap, KT0913_REG_TUNE, &tune_reg); > + > + if (ret) > + return ret; > + > + *frequency = (tune_reg & KT0913_TUNE_FMCHAN_MASK) * KT0913_FMCHAN_MUL; > + > + return 0; > +} > + > +static int __kt0913_set_fm_frequency(struct kt0913_device *radio, > + unsigned int frequency) > +{ > + return regmap_write(radio->regmap, KT0913_REG_TUNE, > + KT0913_TUNE_FMTUNE_ON | (frequency / KT0913_FMCHAN_MUL)); > +} > + > +/* ************************************************************************* */ > + > +static int __kt0913_set_mute(struct kt0913_device *radio, int on) > +{ > + return regmap_update_bits(radio->regmap, > + KT0913_REG_VOLUME, KT0913_VOLUME_DMUTE_MASK, > + on ? KT0913_VOLUME_DMUTE_ON : KT0913_VOLUME_DMUTE_OFF); > +} > + > +/* ************************************************************************* */ > + > +static int __kt0913_set_deemphasis(struct kt0913_device *radio, s32 deemp) > +{ > + switch (deemp) { > + case V4L2_DEEMPHASIS_75_uS: > + return regmap_update_bits(radio->regmap, > + KT0913_REG_VOLUME, KT0913_VOLUME_DE_MASK, > + KT0913_VOLUME_DE_50US); > + > + /* 50us is used for the disabled option (which is not supported > + * on the chip) and the 50uS value > + */ > + default: > + return regmap_update_bits(radio->regmap, > + KT0913_REG_VOLUME, KT0913_VOLUME_DE_MASK, > + KT0913_VOLUME_DE_75US); > + } > +} > + > +/* ************************************************************************* */ > + > +static int __kt0913_set_volume(struct kt0913_device *radio, s32 volume) > +{ > + /* map [-60, 0] to [1, 31] which is what the kt0913 expects */ > + volume = (volume / 2) + 31; > + return regmap_update_bits(radio->regmap, > + KT0913_REG_RXCFG, KT0913_RXCFGA_VOLUME_MASK, > + volume); > +} > + > +/* ************************************************************************* */ > + > +static int __kt0913_set_standby(struct kt0913_device *radio, int standby) > +{ > + return regmap_update_bits(radio->regmap, > + KT0913_REG_RXCFG, KT0913_RXCFGA_STDBY_MASK, > + standby ? KT0913_RXCFGA_STDBY_ON : KT0913_RXCFGA_STDBY_OFF); > +} > + > +/* ************************************************************************* */ > + > +static int __kt0913_get_pll_status(struct kt0913_device *radio, int *locked) > +{ > + unsigned int statusa_reg; > + int ret = regmap_read(radio->regmap, KT0913_REG_STATUSA, &statusa_reg); > + > + if (ret) > + return ret; > + > + *locked = (statusa_reg & KT0913_STATUSA_PLL_LOCK_MASK) == > + KT0913_STATUSA_PLL_LOCK_LOCKED ? 1 : 0; > + > + return 0; > +} > + > +/* ************************************************************************* */ > + > +static int __kt0913_get_rx_stereo_or_mono(struct kt0913_device *radio, > + int *stereo) > +{ > + unsigned int statusa_reg; > + int ret = regmap_read(radio->regmap, KT0913_REG_STATUSA, &statusa_reg); > + > + if (ret) > + return ret; > + > + *stereo = (statusa_reg & KT0913_STATUSA_ST_MASK) == > + KT0913_STATUSA_ST_STEREO ? 1 : 0; > + > + return 0; > +} > + > +/* ************************************************************************* */ > + > +static int __kt0913_get_fm_rssi(struct kt0913_device *radio, s32 *rssi) > +{ > + unsigned int statusa_reg; > + int ret = regmap_read(radio->regmap, KT0913_REG_STATUSA, &statusa_reg); > + > + if (ret) > + return ret; > + > + /* RSSI(dBm) = -100 + FMRSSI<4:0> * 3dBm > + * even tho we can get the value in dBm, we want a % > + */ > + *rssi = (statusa_reg & KT0913_STATUSA_FMRSSI_MASK) >> > + KT0913_STATUSA_FMRSSI_SHIFT; > + /* map range 0-31 to 0-65535 */ > + *rssi *= 65535; > + *rssi /= KT0913_STATUSA_FMRSSI_MASK >> KT0913_STATUSA_FMRSSI_SHIFT; > + > + return 0; > +} > + > +/* ************************************************************************* */ > + > +static int __kt0913_get_cfg_stereo_enabled(struct kt0913_device *radio, > + int *stereo) > +{ > + unsigned int dspcfga_reg; > + int ret = regmap_read(radio->regmap, KT0913_REG_DSPCFGA, &dspcfga_reg); > + > + if (ret) > + return ret; > + > + *stereo = (dspcfga_reg & KT0913_DSPCFGA_MONO_MASK) == > + KT0913_DSPCFGA_MONO_OFF ? 1 : 0; > + > + return ret; > +} > + > +static int __kt0913_set_cfg_stereo_enabled(struct kt0913_device *radio, > + int stereo) > +{ > + return regmap_update_bits(radio->regmap, > + KT0913_REG_DSPCFGA, KT0913_DSPCFGA_MONO_MASK, > + stereo ? KT0913_DSPCFGA_MONO_OFF : KT0913_DSPCFGA_MONO_ON); > +} > + > +/* ************************************************************************* */ > + > +static int __kt0913_set_au_gain(struct kt0913_device *radio, s32 gain) > +{ > + switch (gain) { > + case 6: > + return regmap_update_bits(radio->regmap, > + KT0913_REG_AMSYSCFG, KT0913_AMSYSCFG_AU_GAIN_MASK, > + KT0913_AMSYSCFG_AU_GAIN_6DB); > + case 3: > + return regmap_update_bits(radio->regmap, > + KT0913_REG_AMSYSCFG, KT0913_AMSYSCFG_AU_GAIN_MASK, > + KT0913_AMSYSCFG_AU_GAIN_3DB); > + case 0: > + return regmap_update_bits(radio->regmap, > + KT0913_REG_AMSYSCFG, KT0913_AMSYSCFG_AU_GAIN_MASK, > + KT0913_AMSYSCFG_AU_GAIN_0DB); > + case -3: > + return regmap_update_bits(radio->regmap, > + KT0913_REG_AMSYSCFG, KT0913_AMSYSCFG_AU_GAIN_MASK, > + KT0913_AMSYSCFG_AU_GAIN_MIN_3DB); > + default: > + return -EINVAL; > + } > +} > + > +/* ************************************************************************* */ > + > +static int __kt0913_set_am_fm_band(struct kt0913_device *radio, > + unsigned int band) > +{ > + return regmap_update_bits(radio->regmap, > + KT0913_REG_AMSYSCFG, KT0913_AMSYSCFG_AM_FM_MASK, > + band == BAND_AM ? > + KT0913_AMSYSCFG_AM_FM_AM : KT0913_AMSYSCFG_AM_FM_FM); > +} > + > +/* ************************************************************************* */ > + > +static int __kt0913_get_am_frequency(struct kt0913_device *radio, > + unsigned int *frequency) > +{ > + unsigned int amchan_reg; > + int ret = regmap_read(radio->regmap, KT0913_REG_AMCHAN, &amchan_reg); > + > + if (ret) > + return ret; > + > + *frequency = (amchan_reg & KT0913_AMCHAN_AMCHAN_MASK); > + > + return 0; > +} > + > +static int __kt0913_set_am_frequency(struct kt0913_device *radio, > + unsigned int frequency) > +{ > + return regmap_write(radio->regmap, KT0913_REG_AMCHAN, > + KT0913_AMCHAN_AMTUNE_ON | frequency); > +} > + > +/* ************************************************************************* */ > + > +static int __kt0913_get_am_rssi(struct kt0913_device *radio, s32 *rssi) > +{ > + unsigned int amstatusa_reg; > + int ret = regmap_read(radio->regmap, > + KT0913_REG_AMSTATUSA, &amstatusa_reg); > + > + if (ret) > + return ret; > + > + /* AMRSSI(dBm) = -90 + AMRSSI<4:0> * 3dBm > + * even tho we can get the value in dBm, we want a % > + */ > + *rssi = (amstatusa_reg & KT0913_AMSTATUSA_AMRSSI_MASK) >> > + KT0913_AMSTATUSA_AMRSSI_SHIFT; > + /* map range 0-31 to 0-65535 */ > + *rssi *= 65535; > + *rssi /= KT0913_AMSTATUSA_AMRSSI_MASK >> KT0913_AMSTATUSA_AMRSSI_SHIFT; > + > + return 0; > +} > + > +/* ************************************************************************* */ > + > +static int __kt0913_init(struct kt0913_device *radio) > +{ > + int ret = 0; > + > + /* write the defaults */ > + ret = regmap_multi_reg_write(radio->regmap, > + kt0913_init_regs_to_defaults, > + ARRAY_SIZE(kt0913_init_regs_to_defaults)); > + if (ret) { > + v4l2_err(radio->client, > + "regmap_multi_reg_write() failed! %d", ret); > + return ret; > + } > + > + /* set the audio dac anti-pop config */ > + ret = regmap_update_bits(radio->regmap, > + KT0913_REG_VOLUME, KT0913_VOLUME_POP_MASK, > + radio->audio_anti_pop << > + KT0913_VOLUME_POP_SHIFT); > + if (ret) { > + v4l2_err(radio->client, > + "regmap_update_bits() err on anti-pop cfg! %d", ret); > + return ret; > + } > + > + /* set the reference clock config */ > + ret = regmap_update_bits(radio->regmap, > + KT0913_REG_AMSYSCFG, > + KT0913_AMSYSCFG_REFCLK_MASK, > + radio->refclock_val << > + KT0913_AMSYSCFG_REFCLK_SHIFT); > + if (ret) { > + v4l2_err(radio->client, > + "regmap_update_bits() err on refclk cfg! %d", ret); > + return ret; > + } > + > + if (kt0913_use_campus_band) { > + v4l2_info(radio->client, > + "campus band is enabled!"); > + /* set the campus band bit */ > + ret = regmap_update_bits(radio->regmap, > + KT0913_REG_LOCFGC, > + KT0913_LOCFG_CAMPUSBAND_EN_MASK, > + KT0913_LOCFG_CAMPUSBAND_EN_ON); > + if (ret) { > + v4l2_err(radio->client, > + "regmap_update_bits() err on campus band! %d", > + ret); > + return ret; > + } > + } > + > + return __kt0913_set_mute(radio, true); > +} > + > +/* ************************************************************************* */ > + > +static int kt0913_ioctl_vidioc_g_frequency(struct file *file, void *priv, > + struct v4l2_frequency *f) > +{ > + struct kt0913_device *radio = video_drvdata(file); > + int ret; > + > + if (f->tuner != 0) > + return -EINVAL; > + > + f->type = V4L2_TUNER_RADIO; > + > + if (radio->band == BAND_AM) > + ret = __kt0913_get_am_frequency(radio, &f->frequency); > + else > + ret = __kt0913_get_fm_frequency(radio, &f->frequency); > + > + if (ret) > + return ret; > + > + /* convert kHz freq into v4l2 freq */ > + f->frequency = khz_to_v4l2_freq(f->frequency); > + > + return 0; > +} > + > +static int kt0913_ioctl_vidioc_s_frequency(struct file *file, void *priv, > + const struct v4l2_frequency *f) > +{ > + struct kt0913_device *radio = video_drvdata(file); > + unsigned int freq = f->frequency; > + unsigned int new_band; > + unsigned int half_gap_freq; > + int ret; > + > + if (f->tuner != 0 || f->type != V4L2_TUNER_RADIO) > + return -EINVAL; > + > + if (freq == 0) > + return -EINVAL; > + > + /* calculate the middle frequency on the gap of the bands */ > + half_gap_freq = kt0913_bands[BAND_AM].rangehigh; > + > + if (kt0913_use_campus_band) > + /* using the AM band and the campus FM band if enabled */ > + half_gap_freq += kt0913_bands[BAND_FM_CAMUS].rangelow; > + else > + /* or using the AM band and the standard FM band */ > + half_gap_freq += kt0913_bands[BAND_FM].rangelow; > + > + half_gap_freq /= 2; > + > + if (freq <= half_gap_freq) > + new_band = BAND_AM; > + else if (freq <= kt0913_bands[BAND_FM].rangelow && > + kt0913_use_campus_band) > + new_band = BAND_FM_CAMUS; > + else > + new_band = BAND_FM; > + > + /* is the requested band different than the one currently set? */ > + if (radio->band != new_band) { > + /* update the band on the device */ > + ret = __kt0913_set_am_fm_band(radio, new_band); > + if (ret) > + return ret; > + radio->band = new_band; > + } > + > + /* clamp the frequency to the band boundaries */ > + freq = clamp(freq, kt0913_bands[new_band].rangelow, > + kt0913_bands[new_band].rangehigh); > + > + /* convert v4l2 freq to kHz */ > + freq = v4l2_freq_to_khz(freq); > + > + if (radio->band == BAND_AM) > + return __kt0913_set_am_frequency(radio, freq); > + else > + return __kt0913_set_fm_frequency(radio, freq); > +} > + > +static int kt0913_ioctl_vidioc_enum_freq_bands(struct file *file, void *priv, > + struct v4l2_frequency_band *band) > +{ > + if (band->tuner != 0) > + return -EINVAL; > + > + switch (band->index) { > + case 0: > + if (kt0913_use_campus_band) > + *band = kt0913_bands[BAND_FM_CAMUS]; > + else > + *band = kt0913_bands[BAND_FM]; > + return 0; > + case 1: > + *band = kt0913_bands[BAND_AM]; > + return 0; > + default: > + return -EINVAL; > + } > +} > + > +/* ************************************************************************* */ > + > +/* V4L2 vidioc */ > +static int kt0913_ioctl_vidioc_querycap(struct file *file, void *priv, > + struct v4l2_capability *capability) > +{ > + struct kt0913_device *radio = video_drvdata(file); > + struct video_device *dev; > + > + if (!radio) > + return -ENODEV; > + > + dev = &radio->vdev; > + > + if (!dev) > + return -ENODEV; > + > + strscpy(capability->driver, KT0913_FM_AM_DRIVER_NAME, > + sizeof(capability->driver)); > + strscpy(capability->card, dev->name, sizeof(capability->card)); > + snprintf(capability->bus_info, sizeof(capability->bus_info), > + "I2C:%s", dev_name(&dev->dev)); > + return 0; > +} > + > +static int kt0913_ioctl_vidioc_g_tuner(struct file *file, void *priv, > + struct v4l2_tuner *v) > +{ > + struct kt0913_device *radio = video_drvdata(file); > + int ret; > + int stereo_enabled; > + int is_stereo; > + > + if (v->index != 0) > + return -EINVAL; > + > + strscpy(v->name, "FM/AM", sizeof(v->name)); > + v->type = V4L2_TUNER_RADIO; > + > + v->capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO | > + V4L2_TUNER_CAP_FREQ_BANDS; > + > + v->rangelow = kt0913_bands[BAND_AM].rangelow; > + v->rangehigh = kt0913_bands[BAND_FM].rangehigh; > + > + if (radio->band == BAND_AM) { > + v->rxsubchans = V4L2_TUNER_SUB_MONO; > + v->audmode = V4L2_TUNER_MODE_MONO; > + > + ret = __kt0913_get_am_rssi(radio, &v->signal); > + if (ret) > + return ret; > + } else { > + ret = __kt0913_get_cfg_stereo_enabled(radio, &stereo_enabled); > + if (ret) > + return ret; > + > + v->rxsubchans = stereo_enabled ? > + V4L2_TUNER_SUB_STEREO : V4L2_TUNER_SUB_MONO; > + > + ret = __kt0913_get_rx_stereo_or_mono(radio, &is_stereo); > + if (ret) > + return ret; > + > + v->audmode = is_stereo ? > + V4L2_TUNER_MODE_STEREO : V4L2_TUNER_MODE_MONO; > + > + ret = __kt0913_get_fm_rssi(radio, &v->signal); > + if (ret) > + return ret; > + } > + > + /* AFC is enabled and active by default */ > + v->afc = 1; > + > + return 0; > +} > + > +static int kt0913_ioctl_vidioc_s_tuner(struct file *file, void *priv, > + const struct v4l2_tuner *v) > +{ > + struct kt0913_device *radio = video_drvdata(file); > + > + if (v->index != 0) > + return -EINVAL; > + > + /* only mono and stereo are supported */ > + if (v->audmode != V4L2_TUNER_MODE_MONO && > + v->audmode != V4L2_TUNER_MODE_STEREO) > + return 0; > + > + /* AM is mono only, so don't try to set it to stereo */ > + if (radio->band == BAND_AM && v->audmode != V4L2_TUNER_MODE_MONO) > + return 0; > + > + /* set to stereo if specified, otherwise set to mono */ > + return __kt0913_set_cfg_stereo_enabled(radio, > + v->audmode == V4L2_TUNER_MODE_STEREO); > +} > + > +static int kt0913_s_ctrl(struct v4l2_ctrl *ctrl) > +{ > + struct kt0913_device *radio = v4l2_ctrl_to_device(ctrl); > + > + switch (ctrl->id) { > + case V4L2_CID_AUDIO_MUTE: > + return __kt0913_set_mute(radio, ctrl->val); > + case V4L2_CID_AUDIO_VOLUME: > + return __kt0913_set_volume(radio, ctrl->val); > + case V4L2_CID_GAIN: > + return __kt0913_set_au_gain(radio, ctrl->val); > + case V4L2_CID_TUNE_DEEMPHASIS: > + return __kt0913_set_deemphasis(radio, ctrl->val); > + default: > + return -EINVAL; > + } > +} > + > +static int kt0913_g_volatile_ctrl(struct v4l2_ctrl *ctrl) > +{ > + struct kt0913_device *radio = v4l2_ctrl_to_device(ctrl); > + > + switch (ctrl->id) { > + case V4L2_CID_RF_TUNER_PLL_LOCK: > + return __kt0913_get_pll_status(radio, &ctrl->val); > + default: > + return -EINVAL; > + } > +} > + > +static const struct v4l2_ctrl_ops kt0913_ctrl_ops = { > + .s_ctrl = kt0913_s_ctrl, > + .g_volatile_ctrl = kt0913_g_volatile_ctrl, > +}; > + > +/* ************************************************************************* */ > + > +/* File system interface (use the ancillary fops for v4l2) */ > +static const struct v4l2_file_operations kt0913_radio_fops = { > + .owner = THIS_MODULE, > + .open = v4l2_fh_open, > + .release = v4l2_fh_release, > + .poll = v4l2_ctrl_poll, > + .unlocked_ioctl = video_ioctl2, > +}; > + > +/* ioctl ops */ > +static const struct v4l2_ioctl_ops kt0913_ioctl_ops = { > + .vidioc_querycap = kt0913_ioctl_vidioc_querycap, > + .vidioc_g_tuner = kt0913_ioctl_vidioc_g_tuner, > + .vidioc_s_tuner = kt0913_ioctl_vidioc_s_tuner, > + .vidioc_g_frequency = kt0913_ioctl_vidioc_g_frequency, > + .vidioc_s_frequency = kt0913_ioctl_vidioc_s_frequency, > + .vidioc_enum_freq_bands = kt0913_ioctl_vidioc_enum_freq_bands, > + /* use ancillary functions for these: */ > + .vidioc_log_status = v4l2_ctrl_log_status, > + .vidioc_subscribe_event = v4l2_ctrl_subscribe_event, > + .vidioc_unsubscribe_event = v4l2_event_unsubscribe, > +}; > + > +/* V4L2 RADIO device structure */ > +static struct video_device kt0913_radio_template = { > + .name = KT0913_FM_AM_DRIVER_NAME, > + .fops = &kt0913_radio_fops, > + .ioctl_ops = &kt0913_ioctl_ops, > + .release = video_device_release_empty, > + .vfl_dir = VFL_DIR_RX, > + .device_caps = V4L2_CAP_TUNER | V4L2_CAP_RADIO, > +}; > + > +/* ************************************************************************* */ > + > +#if IS_ENABLED(CONFIG_OF) > +static const struct of_device_id kt0913_of_match[] = { > + { .compatible = "ktm,kt0913" }, > + { /* sentinel */ } > +}; > +MODULE_DEVICE_TABLE(of, kt0913_of_match); > +#endif /* IS_ENABLED(CONFIG_OF) */ > + > +static void __kt0913_parse_dt(struct kt0913_device *radio) > +{ > + const void *ptr_anti_pop = of_get_property(radio->client->dev.of_node, > + "ktm,anti-pop", NULL); > + const void *ptr_refclk = of_get_property(radio->client->dev.of_node, > + "ktm,refclk", NULL); > + > + if (ptr_anti_pop) { > + radio->audio_anti_pop = > + clamp(be32_to_cpup(ptr_anti_pop), 0U, 3U); > + } else { > + radio->audio_anti_pop = 0; > + v4l2_warn(radio->client, > + "No ktm,anti-pop on dt node, using default"); > + } > + > + if (ptr_refclk) { > + radio->refclock_val = > + clamp(be32_to_cpup(ptr_refclk), 0U, 9U); > + } else { > + radio->refclock_val = 0; > + v4l2_warn(radio->client, > + "No ktm,refclk on dt node, using default"); > + } > +} > + > +/* ************************************************************************* */ > + > +static int kt0913_probe(struct i2c_client *client, > + const struct i2c_device_id *id) > +{ > + struct kt0913_device *radio; > + struct v4l2_device *v4l2_dev; > + struct v4l2_ctrl_handler *hdl; > + struct regmap *regmap; > + int ret; > + > + pr_debug("%s\n", __func__); > + > + /* this driver uses word R/W i2c operations, check if it's supported */ > + ret = i2c_check_functionality(client->adapter, > + I2C_FUNC_SMBUS_READ_WORD_DATA | > + I2C_FUNC_SMBUS_WRITE_WORD_DATA); > + if (!ret) { > + v4l2_err(client, > + "I2C adapter doesn't support word operations"); > + return -EIO; > + } > + > + /* check if the device exist on the bus before initializing it */ > + ret = i2c_smbus_read_word_data(client, KT0913_REG_CHIP_ID); > + if (ret < 0) { > + v4l2_err(client, > + "Error reading CHIP ID of the kt0913 (%d)", ret); > + return ret; > + } > + > + /* check if the CHIP ID register value matches the expected value */ > + if (ret != KT0913_CHIP_ID) { > + v4l2_err(radio->client, > + "Invalid CHIP ID: 0x%x, expected 0x%x", > + ret, KT0913_CHIP_ID); > + return -ENODEV; > + } > + > + v4l2_info(client, > + "kt0913 found @ 0x%x (%s)\n", > + client->addr, client->adapter->name); > + > + /* alloc context for the kt0913 radio struct */ > + radio = devm_kzalloc(&client->dev, sizeof(*radio), GFP_KERNEL); > + if (!radio) > + return -ENOMEM; > + > + v4l2_dev = &radio->v4l2_dev; > + ret = v4l2_device_register(&client->dev, v4l2_dev); > + if (ret < 0) { > + v4l2_err(client, > + "could not register v4l2_dev\n"); > + goto errfr; > + } > + > + mutex_init(&radio->mutex); > + > + /* register the control handler from the context struct */ > + hdl = &radio->ctrl_handler; > + v4l2_ctrl_handler_init(hdl, 5); > + > + /* add the control: Mute */ > + radio->ctrl_mute = v4l2_ctrl_new_std(hdl, &kt0913_ctrl_ops, > + V4L2_CID_AUDIO_MUTE, > + 0, 1, 1, 0); > + if (hdl->error) { > + ret = hdl->error; > + v4l2_err(v4l2_dev, "Could not register control: mute\n"); > + goto errunreg; > + } > + > + /* add the control: Volume */ > + radio->ctrl_volume = v4l2_ctrl_new_std(hdl, &kt0913_ctrl_ops, > + V4L2_CID_AUDIO_VOLUME, > + -60, 0, 2, 0); > + if (hdl->error) { > + ret = hdl->error; > + v4l2_err(v4l2_dev, "Could not register control: Volume\n"); > + goto errunreg; > + } > + > + /* add the control: audio gain */ > + radio->ctrl_au_gain = v4l2_ctrl_new_std(hdl, &kt0913_ctrl_ops, > + V4L2_CID_GAIN, > + -3, 6, 3, 3); > + if (hdl->error) { > + ret = hdl->error; > + v4l2_err(v4l2_dev, "Could not register control: audio gain\n"); > + goto errunreg; > + } > + radio->ctrl_au_gain->flags |= V4L2_CTRL_FLAG_SLIDER; > + > + /* add the control: PLL Lock */ > + radio->ctrl_pll_lock = v4l2_ctrl_new_std(hdl, &kt0913_ctrl_ops, > + V4L2_CID_RF_TUNER_PLL_LOCK, > + 0, 1, 1, 0); > + if (hdl->error) { > + ret = hdl->error; > + v4l2_err(v4l2_dev, "Could not register control: pll lock\n"); > + goto errunreg; > + } > + radio->ctrl_pll_lock->flags |= (V4L2_CTRL_FLAG_VOLATILE | > + V4L2_CTRL_FLAG_READ_ONLY); > + > + /* add the control: deemphasis */ > + radio->ctrl_deemp = v4l2_ctrl_new_std_menu(hdl, &kt0913_ctrl_ops, > + V4L2_CID_TUNE_DEEMPHASIS, > + V4L2_DEEMPHASIS_75_uS, > + 0, > + V4L2_DEEMPHASIS_75_uS); > + if (hdl->error) { > + ret = hdl->error; > + v4l2_err(v4l2_dev, "Could not register control: deemphasis\n"); > + goto errunreg; > + } > + /* the control handler is ready to be used */ > + v4l2_dev->ctrl_handler = hdl; > + > + radio->vdev = kt0913_radio_template; > + radio->vdev.lock = &radio->mutex; > + radio->vdev.v4l2_dev = v4l2_dev; > + video_set_drvdata(&radio->vdev, radio); > + > + radio->client = client; > + i2c_set_clientdata(client, radio); > + > + /* init the regmap of the kt0913 */ > + regmap = devm_regmap_init_i2c(client, &kt0913_regmap_config); > + if (IS_ERR(regmap)) { > + ret = PTR_ERR(regmap); > + v4l2_err(client, > + "devm_regmap_init_i2c() failed! %d", ret); > + goto errunreg; > + } > + radio->regmap = regmap; > + > + __kt0913_parse_dt(radio); > + > + /* init the kt0913 into a known state */ > + ret = __kt0913_init(radio); > + if (ret) { > + v4l2_err(client, > + "__kt0913_init() failed! %d", ret); > + goto errunreg; > + } > + > + pm_runtime_get_noresume(&client->dev); > + pm_runtime_set_active(&client->dev); > + pm_runtime_enable(&client->dev); > + pm_runtime_dont_use_autosuspend(&client->dev); > + > + ret = video_register_device(&radio->vdev, > + VFL_TYPE_RADIO, kt0913_v4l2_radio_nr); > + if (ret < 0) { > + v4l2_err(client, > + "Could not register video device!"); > + goto error_pm_disable; > + } > + > + v4l2_info(client, "registered."); > + return 0; > +error_pm_disable: > + pm_runtime_disable(&client->dev); > + pm_runtime_set_suspended(&client->dev); > +errunreg: > + v4l2_ctrl_handler_free(hdl); > + v4l2_device_unregister(v4l2_dev); > +errfr: > + __kt0913_set_standby(radio, true); > + kfree(radio); > + return ret; > +} > + > +static int kt0913_remove(struct i2c_client *client) > +{ > + struct kt0913_device *radio = i2c_get_clientdata(client); > + > + pr_debug("%s\n", __func__); > + if (!radio) > + return -EINVAL; > + > + __kt0913_set_standby(radio, true); > + > + pm_runtime_get_sync(&client->dev); > + pm_runtime_disable(&client->dev); > + pm_runtime_set_suspended(&client->dev); > + pm_runtime_put_noidle(&client->dev); > + > + video_unregister_device(&radio->vdev); > + v4l2_ctrl_handler_free(&radio->ctrl_handler); > + v4l2_device_unregister(&radio->v4l2_dev); > + > + v4l2_info(client, "removed."); > + return 0; > +} > + > +/* ************************************************************************* */ > + > +#ifdef CONFIG_PM > +static int kt0913_i2c_pm_runtime_suspend(struct device *dev) > +{ > + struct kt0913_device *radio = i2c_get_clientdata(to_i2c_client(dev)); > + > + pr_debug("%s\n", __func__); > + if (!radio) > + return 0; > + > + return __kt0913_set_standby(radio, true); > +} > + > +static int kt0913_i2c_pm_runtime_resume(struct device *dev) > +{ > + struct kt0913_device *radio = i2c_get_clientdata(to_i2c_client(dev)); > + > + pr_debug("%s\n", __func__); > + if (!radio) > + return 0; > + > + return __kt0913_set_standby(radio, false); > +} > +#endif /* CONFIG_PM */ > + > +static const struct dev_pm_ops kt0913_i2c_pm_ops = { > + SET_RUNTIME_PM_OPS(kt0913_i2c_pm_runtime_suspend, > + kt0913_i2c_pm_runtime_resume, NULL) > +}; > + > +static const struct i2c_device_id kt0913_idtable[] = { > + { "kt0913", 0 }, > + { /* sentinel */ } > +}; > +MODULE_DEVICE_TABLE(i2c, kt0913_idtable); > + > +static struct i2c_driver kt0913_driver = { > + .driver = { > + .name = "kt0913", > + .of_match_table = of_match_ptr(kt0913_of_match), > + .pm = &kt0913_i2c_pm_ops, > + }, > + .probe = kt0913_probe, > + .remove = kt0913_remove, > + .id_table = kt0913_idtable, > +}; > +module_i2c_driver(kt0913_driver); > + > +MODULE_AUTHOR("Santiago Hormazabal <santiagohssl@gmail.com>"); > +MODULE_DESCRIPTION("KTMicro KT0913 AM/FM receiver"); > +MODULE_LICENSE("GPL"); > +MODULE_VERSION("0.0.2"); > + > +module_param(kt0913_use_campus_band, int, 0); > +MODULE_PARM_DESC(kt0913_use_campus_band, "Use the Campus Band feature (FM range 32MHz-110MHz)"); > +module_param(kt0913_v4l2_radio_nr, int, 0); > +MODULE_PARM_DESC(kt0913_v4l2_radio_nr, "v4l2 device number to use (i.e. /dev/radioX)"); >
On Mon, 3 Aug 2020 at 10:05, Hans Verkuil <hverkuil@xs4all.nl> wrote: > > On 03/08/2020 04:09, Santiago Hormazabal wrote: > > This chip requires almost no support components and can used over I2C. > > The driver uses the I2C bus and exposes the controls as a V4L2 radio. > > Tested with a module that contains this chip (from SZZSJDZ.com, > > part number ZJ-801B, even tho the company seems defunct now), and an H2+ > > AllWinner SoC running a kernel built off 8f2a4a9 of the media_tree. > > > > Signed-off-by: Santiago Hormazabal <santiagohssl@gmail.com> > > --- > > It's good practice to list the changes you made for a v2 either in the > cover letter or here (below ---). > > I have a preference myself for the cover letter, but either is fine. > Will do! > > drivers/media/radio/Kconfig | 10 + > > drivers/media/radio/Makefile | 1 + > > drivers/media/radio/radio-kt0913.c | 1196 ++++++++++++++++++++++++++++ > > 3 files changed, 1207 insertions(+) > > create mode 100644 drivers/media/radio/radio-kt0913.c > > > > diff --git a/drivers/media/radio/Kconfig b/drivers/media/radio/Kconfig > > index d29e29645e04..ac9053a95f3a 100644 > > --- a/drivers/media/radio/Kconfig > > +++ b/drivers/media/radio/Kconfig > > @@ -226,6 +226,16 @@ config RADIO_WL1273 > > # TI's ST based wl128x FM radio > > source "drivers/media/radio/wl128x/Kconfig" > > > > +config RADIO_KT0913 > > + tristate "KT0913 I2C FM/AM radio support" > > + depends on I2C && VIDEO_V4L2 > > + help > > + Say Y here if you want to use the KT0913 FM/AM chip. > > + This is a low cost chip that uses the I2C bus. > > + > > + To compile this driver as a module, choose M here: the > > + module will be called radio-kt0913. > > + > > # > > # ISA drivers configuration > > # > > diff --git a/drivers/media/radio/Makefile b/drivers/media/radio/Makefile > > index 53c7ae135460..a314121f7771 100644 > > --- a/drivers/media/radio/Makefile > > +++ b/drivers/media/radio/Makefile > > @@ -34,5 +34,6 @@ obj-$(CONFIG_RADIO_WL1273) += radio-wl1273.o > > obj-$(CONFIG_RADIO_WL128X) += wl128x/ > > obj-$(CONFIG_RADIO_TEA575X) += tea575x.o > > obj-$(CONFIG_USB_RAREMONO) += radio-raremono.o > > +obj-$(CONFIG_RADIO_KT0913) += radio-kt0913.o > > > > shark2-objs := radio-shark2.o radio-tea5777.o > > diff --git a/drivers/media/radio/radio-kt0913.c b/drivers/media/radio/radio-kt0913.c > > new file mode 100644 > > index 000000000000..e8c1ff882779 > > --- /dev/null > > +++ b/drivers/media/radio/radio-kt0913.c > > @@ -0,0 +1,1196 @@ > > +// SPDX-License-Identifier: GPL-2.0-only > > +/* > > + * drivers/media/radio/radio-kt0913.c > > + * > > + * Driver for the KT0913 radio chip from KTMicro. > > + * This driver provides a v4l2 interface to the tuner, using the I2C > > + * protocol to communicate with the chip. > > + * It exposes two bands, one for AM and another for FM. If the "campus > > + * band" feature needs to be enabled, set the corresponding module parameter > > + * to 1. > > + * Reference Clock and Audio DAC anti-pop configurations should be > > + * set via a device tree node. Defaults will be used otherwise. > > + * > > + * Audio output should be routed to a speaker or an audio capture > > + * device. > > + * > > + * Based on radio-tea5764 by Fabio Belavenuto <belavenuto@gmail.com> > > + * > > + * Copyright (c) 2020 Santiago Hormazabal <santiagohssl@gmail.com> > > + * > > + * TODO: > > + * use rd and wr support for the regmap instead of volatile regs. > > + * add support for the hardware-assisted frequency seek. > > + * export FM SNR and AM/FM AFC deviation values as RO controls. > > + */ > > + > > +#include <linux/module.h> > > +#include <linux/i2c.h> > > +#include <linux/err.h> > > +#include <linux/pm_runtime.h> > > +#include <linux/of.h> > > +#include <linux/math64.h> > > +#include <linux/regmap.h> > > +#include <media/v4l2-ctrls.h> > > +#include <media/v4l2-device.h> > > +#include <media/v4l2-ioctl.h> > > +#include <media/v4l2-event.h> > > + > > + /* ************************************************************************* */ > > + > > + /* registers of the kt0913 */ > > +#define KT0913_REG_CHIP_ID 0x01 > > +#define KT0913_REG_SEEK 0x02 > > +#define KT0913_REG_TUNE 0x03 > > +#define KT0913_REG_VOLUME 0x04 > > +#define KT0913_REG_DSPCFGA 0x05 > > +#define KT0913_REG_LOCFGA 0x0A > > +#define KT0913_REG_LOCFGC 0x0C > > +#define KT0913_REG_RXCFG 0x0F > > +#define KT0913_REG_STATUSA 0x12 > > +#define KT0913_REG_STATUSB 0x13 > > +#define KT0913_REG_STATUSC 0x14 > > +#define KT0913_REG_AMSYSCFG 0x16 > > +#define KT0913_REG_AMCHAN 0x17 > > +#define KT0913_REG_AMCALI 0x18 > > +#define KT0913_REG_GPIOCFG 0x1D > > +#define KT0913_REG_AMDSP 0x22 > > +#define KT0913_REG_AMSTATUSA 0x24 > > +#define KT0913_REG_AMSTATUSB 0x25 > > +#define KT0913_REG_SOFTMUTE 0x2E > > +#define KT0913_REG_AMCFG 0x33 > > +#define KT0913_REG_AMCFG2 0x34 > > +#define KT0913_REG_AFC 0x3C > > Hmm, this is still uppercase. > Uh! you know what? I had all the changes staged and then when I upgraded the master of the repo I forgot to pop them. Then I continued fixing the other errors you've mentioned before. This means that the upper case hex and "CAMUS" are still there. I'll fix them on V3 :( > > + > > +/* register symbols masks, values and shift count */ > > +#define KT0913_TUNE_FMTUNE_MASK 0x8000 /* FM Tune enable */ > > +#define KT0913_TUNE_FMTUNE_ON 0x8000 /* FM Tune enabled */ > > +#define KT0913_TUNE_FMTUNE_OFF 0x0000 /* FM Tune disabled */ > > +#define KT0913_TUNE_FMCHAN_MASK 0x0FFF /* frequency in kHz / 50kHz */ > > + > > +#define KT0913_VOLUME_DMUTE_MASK 0x2000 > > +#define KT0913_VOLUME_DMUTE_ON 0x0000 > > +#define KT0913_VOLUME_DMUTE_OFF 0x2000 > > +#define KT0913_VOLUME_DE_MASK 0x0800 /* de-emphasis time constant */ > > +#define KT0913_VOLUME_DE_75US 0x0000 /* 75us */ > > +#define KT0913_VOLUME_DE_50US 0x0800 /* 50us */ > > +#define KT0913_VOLUME_POP_MASK 0x30 /* audio dac anti-pop config */ > > +#define KT0913_VOLUME_POP_SHIFT 4 > > + > > +#define KT0913_DSPCFGA_MONO_MASK 0x8000 /* mono select (0=stereo, 1=mono) */ > > +#define KT0913_DSPCFGA_MONO_ON 0x8000 /* mono */ > > +#define KT0913_DSPCFGA_MONO_OFF 0x0000 /* stereo */ > > + > > +#define KT0913_LOCFG_CAMPUSBAND_EN_MASK 0x0008 /* campus band fm enable */ > > +#define KT0913_LOCFG_CAMPUSBAND_EN_ON 0x0008 /* FM range 64-110MHz */ > > +#define KT0913_LOCFG_CAMPUSBAND_EN_OFF 0x0000 /* FM range 32-110MHz */ > > + > > +#define KT0913_RXCFGA_STDBY_MASK 0x1000 /* standby mode enable */ > > +#define KT0913_RXCFGA_STDBY_ON 0x1000 /* standby mode enabled */ > > +#define KT0913_RXCFGA_STDBY_OFF 0x0000 /* standby mode disabled */ > > +#define KT0913_RXCFGA_VOLUME_MASK 0x001F /* volume control */ > > + > > +#define KT0913_STATUSA_XTAL_OK 0x8000 /* crystal ready indicator */ > > +#define KT0913_STATUSA_STC 0x4000 /* seek/tune complete */ > > + > > +#define KT0913_STATUSA_PLL_LOCK_MASK 0x800 /* system pll ready indicator */ > > +#define KT0913_STATUSA_PLL_LOCK_LOCKED 0x800 /* system pll ready */ > > +#define KT0913_STATUSA_PLL_LOCK_UNLOCKED 0x000 /* not ready */ > > +#define KT0913_STATUSA_LO_LOCK 0x400 /* LO synthesizer ready indicator */ > > +#define KT0913_STATUSA_ST_MASK 0x300 /* stereo indicator (0x300=stereo, otherwise mono) */ > > +#define KT0913_STATUSA_ST_STEREO 0x300 /* stereo */ > > +#define KT0913_STATUSA_FMRSSI_MASK 0xF8 /* FM RSSI (-100dBm + FMRSSI*3dBm) */ > > +#define KT0913_STATUSA_FMRSSI_SHIFT 3 > > + > > +#define KT0913_STATUSC_PWSTATUS 0x8000 /* power status indicator */ > > +#define KT0913_STATUSC_CHIPRDY 0x2000 /* chip ready indicator */ > > +#define KT0913_STATUSC_FMSNR 0x1FC0 /* FM SNR (unknown units) */ > > + > > +#define KT0913_AMCHAN_AMTUNE_MASK 0x8000 /* AM tune enable */ > > +#define KT0913_AMCHAN_AMTUNE_ON 0x8000 /* AM tune enabled */ > > +#define KT0913_AMCHAN_AMTUNE_OFF 0x0000 /* AM tune disabled */ > > +#define KT0913_AMCHAN_AMCHAN_MASK 0x7FF /* am channel in kHz */ > > + > > +#define KT0913_AMSYSCFG_AM_FM_MASK 0x8000 /* am/fm mode control */ > > +#define KT0913_AMSYSCFG_AM_FM_AM 0x8000 /* am mode */ > > +#define KT0913_AMSYSCFG_AM_FM_FM 0x0000 /* fm mode (default) */ > > +#define KT0913_AMSYSCFG_REFCLK_MASK 0x0F00 /* reference clock selection */ > > +#define KT0913_AMSYSCFG_REFCLK_SHIFT 8 > > +#define KT0913_AMSYSCFG_AU_GAIN_MASK 0x00C0 /* audio gain selection */ > > +#define KT0913_AMSYSCFG_AU_GAIN_6DB 0x0040 /* 6dB audio gain */ > > +#define KT0913_AMSYSCFG_AU_GAIN_3DB 0x0000 /* 3dB audio gain (default) */ > > +#define KT0913_AMSYSCFG_AU_GAIN_0DB 0x00C0 /* 0dB audio gain */ > > +#define KT0913_AMSYSCFG_AU_GAIN_MIN_3DB 0x0080 /* -3dB audio gain */ > > + > > +#define KT0913_AMSTATUSA_AMRSSI_MASK 0x1F00 /* am channel rssi */ > > +#define KT0913_AMSTATUSA_AMRSSI_SHIFT 8 > > + > > +/* constants */ > > +#define KT0913_CHIP_ID 0x544B /* ASCII of 'KT' */ > > + > > +#define V4L2_KHZ_FREQ_MUL 16U /* v4l2 uses 16x the kHz value as their freq */ > > +#define KT0913_FMCHAN_MUL 50U /* kt0913 uses freqs with a 50kHz multiplier */ > > +#define KT0913_FM_RANGE_LOW_NO_CAMPUS 64000U /* 64MHz lower bound for FM */ > > +#define KT0913_FM_RANGE_LOW_CAMPUS 32000U /* 32MHz lower bound for campus FM */ > > +#define KT0913_FM_RANGE_HIGH 110000U /* 110MHz upper bound for FM */ > > +#define KT0913_AM_RANGE_LOW 500U /* 500kHz lower bound for AM */ > > +#define KT0913_AM_RANGE_HIGH 1710U /* 1710kHz upper bound for AM */ > > + > > +#define KT0913_FM_AM_DRIVER_NAME "kt0913-fm-am" > > Just name it "kt0913". Does it just support the 0913 or other variants as well? > I wonder if it should perhaps be named "kt091x". Up to you. > > > + > > +/* ************************************************************************* */ > > + > > +/* v4l2 device number to use. -1 will assign the next free one */ > > +static int kt0913_v4l2_radio_nr = -1; > > +/* use the extended range of FM down to 32MHz. disabled by default */ > > +static int kt0913_use_campus_band; > > + > > +/* ************************************************************************* */ > > + > > +/* kt0913 status struct */ > > +struct kt0913_device { > > + struct v4l2_device v4l2_dev; /* main v4l2 struct */ > > + struct i2c_client *client; /* I2C client */ > > + struct video_device vdev; /* vide_device struct */ > > + struct v4l2_ctrl_handler ctrl_handler; /* ctrl_handler struct */ > > + > > + /* V4L2 Controls */ > > + struct v4l2_ctrl *ctrl_pll_lock; /* PLL lock */ > > + struct v4l2_ctrl *ctrl_volume; /* Overall volume */ > > + struct v4l2_ctrl *ctrl_au_gain; /* Audio Gain */ > > + struct v4l2_ctrl *ctrl_mute; /* Master mute */ > > + struct v4l2_ctrl *ctrl_deemp; /* Deemphasis */ > > + > > + /* current operation band (fm, fm_campus, am) */ > > + unsigned int band; > > + > > + /* audio dac anti-pop setting: > > + * 0 -> 100uF (default) > > + * 1 -> 60uF > > + * 2 -> 20uF > > + * 3 -> 10uF > > + */ > > + unsigned int audio_anti_pop; > > + > > + /* > > + * reference clock selection: > > + * 0 -> 32.768kHz (default) > > + * 1 -> 6.5MHz > > + * 2 -> 7.6MHz > > + * 3 -> 12MHz > > + * 4 -> 13MHz > > + * 5 -> 15.2MHz > > + * 6 -> 19.2MHz > > + * 7 -> 24MHz > > + * 8 -> 26MHz > > + * 9 -> 38kHz > > + */ > > + unsigned int refclock_val; > > + > > + /* Regmap */ > > + struct regmap *regmap; > > + > > + /* For core assisted locking */ > > + struct mutex mutex; > > +}; > > + > > +/* ************************************************************************* */ > > + > > +/* Regmap settings */ > > +static const struct regmap_range kt0913_regmap_all_registers_range[] = { > > + regmap_reg_range(0x01, 0x05), > > + regmap_reg_range(0x0A, 0x0A), > > + regmap_reg_range(0x0C, 0x0C), > > + regmap_reg_range(0x0F, 0x0F), > > + regmap_reg_range(0x12, 0x14), > > + regmap_reg_range(0x16, 0x18), > > + regmap_reg_range(0x1D, 0x1D), > > + regmap_reg_range(0x22, 0x22), > > + regmap_reg_range(0x24, 0x25), > > + regmap_reg_range(0x2E, 0x2F), > > + regmap_reg_range(0x30, 0x34), > > + regmap_reg_range(0x3A, 0x3A), > > + regmap_reg_range(0x3C, 0x3C), > > +}; > > + > > +static const struct regmap_access_table kt0913_all_registers_access_table = { > > + .yes_ranges = kt0913_regmap_all_registers_range, > > + .n_yes_ranges = ARRAY_SIZE(kt0913_regmap_all_registers_range), > > +}; > > + > > +static const struct reg_sequence kt0913_init_regs_to_defaults[] = { > > + /* Standby disabled, volume 0dB */ > > + { KT0913_REG_RXCFG, 0x881F }, > > + /* FM Channel spacing = 50kHz, Right & Left unmuted */ > > + { KT0913_REG_SEEK, 0x000B }, > > + /* Stereo, High Stereo/Mono blend level, blend disabled */ > > + { KT0913_REG_DSPCFGA, 0x1000 }, > > + /* FM AFC Enabled */ > > + { KT0913_REG_LOCFGA, 0x0100 }, > > + /* Campus band disabled by default */ > > + { KT0913_REG_LOCFGC, 0x0024 }, > > + /* > > + * FM mode, internal defined bands, clock from XT, 32.768kHz > > + * 3dB audio gain, AM AFC Enabled > > + */ > > + { KT0913_REG_AMSYSCFG, 0x0002 }, > > + /* Default AM freq = 504kHz */ > > + { KT0913_REG_AMCHAN, 0x01F8}, > > + /* VOL and CH GPIOs set to HiZ */ > > + { KT0913_REG_GPIOCFG, 0x0000 }, > > + /* AM Channel bandwidth = 6kHz, non-differential output */ > > + { KT0913_REG_AMDSP, 0xAFC4 }, > > + /* > > + * softmute is disabled on AM and FM, but set the defaults: > > + * strong softmute attn., slow softmute attack/recover, > > + * lowest AM softumte start level, almost the minimum > > + * softmute target volume, RSSI mode for softmute, lowest > > + * FM softmute start level > > + */ > > + { KT0913_REG_SOFTMUTE, 0x0010 }, > > + /* 1kHz for AM channel space, working mode A for the keys */ > > + { KT0913_REG_AMCFG, 0x1401 }, > > + /* TIME1 = shortest, TIME2 = fastest */ > > + { KT0913_REG_AMCFG, 0x4050 }, > > + /* set 86MHz as the default frequency, and tune it */ > > + { KT0913_REG_TUNE, 0x86B8 }, > > + /* > > + * FM&AM Softmute disabled, Mute disabled, 75us deemp., > > + * no bass boost, 100uF anti pop cap > > + */ > > + { KT0913_REG_VOLUME, 0xE080 }, > > +}; > > + > > +static const struct regmap_config kt0913_regmap_config = { > > + .reg_bits = 8, > > + .val_bits = 16, > > + .max_register = KT0913_REG_AFC, > > + .volatile_table = &kt0913_all_registers_access_table, > > + .cache_type = REGCACHE_RBTREE, > > + .val_format_endian = REGMAP_ENDIAN_BIG, > > +}; > > + > > +/* ************************************************************************* */ > > + > > +/* bands where the kt0913 operates */ > > +enum { BAND_FM, BAND_FM_CAMUS, BAND_AM }; > > This is still CAMUS. I wonder if you reposted v1 instead of v2. > > I stop reviewing because this doesn't seem right. > > Regards, > > Hans > Yes, sorry about this. I'll add my changelong on the cover letter for the next set. Please have some patience, this is my first time doing this :) > > + > > +static const struct v4l2_frequency_band kt0913_bands[] = { > > + { > > + /* BAND_FM */ > > + .type = V4L2_TUNER_RADIO, > > + .index = 0, /* index provided to v4l2 */ > > + .capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO | > > + V4L2_TUNER_CAP_FREQ_BANDS, > > + .rangelow = KT0913_FM_RANGE_LOW_NO_CAMPUS * V4L2_KHZ_FREQ_MUL, > > + .rangehigh = KT0913_FM_RANGE_HIGH * V4L2_KHZ_FREQ_MUL, > > + .modulation = V4L2_BAND_MODULATION_FM, > > + }, > > + { > > + /* BAND_FM_CAMUS */ > > + .type = V4L2_TUNER_RADIO, > > + .index = 0, /* index provided to v4l2 */ > > + .capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO | > > + V4L2_TUNER_CAP_FREQ_BANDS, > > + .rangelow = KT0913_FM_RANGE_LOW_CAMPUS * V4L2_KHZ_FREQ_MUL, > > + .rangehigh = KT0913_FM_RANGE_HIGH * V4L2_KHZ_FREQ_MUL, > > + .modulation = V4L2_BAND_MODULATION_FM, > > + }, > > + { > > + /* BAND_AM */ > > + .type = V4L2_TUNER_RADIO, > > + .index = 1, /* index provided to v4l2 */ > > + .capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_FREQ_BANDS, > > + .rangelow = KT0913_AM_RANGE_LOW * V4L2_KHZ_FREQ_MUL, > > + .rangehigh = KT0913_AM_RANGE_HIGH * V4L2_KHZ_FREQ_MUL, > > + .modulation = V4L2_BAND_MODULATION_AM, > > + }, > > +}; > > + > > +/* ************************************************************************* */ > > + > > +static inline struct kt0913_device * > > +v4l2_device_to_device(struct v4l2_device *v4l2_dev) > > +{ > > + return container_of(v4l2_dev, > > + struct kt0913_device, v4l2_dev); > > +} > > + > > +static inline struct kt0913_device * > > +v4l2_ctrl_to_device(struct v4l2_ctrl *ctrl_handler) > > +{ > > + return container_of(ctrl_handler->handler, > > + struct kt0913_device, ctrl_handler); > > +} > > + > > +/* ************************************************************************* */ > > + > > +static inline u32 khz_to_v4l2_freq(unsigned int freq) > > +{ > > + return freq * V4L2_KHZ_FREQ_MUL; > > +} > > + > > +static inline unsigned int v4l2_freq_to_khz(u32 v4l2_freq) > > +{ > > + return v4l2_freq / V4L2_KHZ_FREQ_MUL; > > +} > > + > > +/* ************************************************************************* */ > > + > > +static int __kt0913_get_fm_frequency(struct kt0913_device *radio, > > + unsigned int *frequency) > > +{ > > + unsigned int tune_reg; > > + int ret = regmap_read(radio->regmap, KT0913_REG_TUNE, &tune_reg); > > + > > + if (ret) > > + return ret; > > + > > + *frequency = (tune_reg & KT0913_TUNE_FMCHAN_MASK) * KT0913_FMCHAN_MUL; > > + > > + return 0; > > +} > > + > > +static int __kt0913_set_fm_frequency(struct kt0913_device *radio, > > + unsigned int frequency) > > +{ > > + return regmap_write(radio->regmap, KT0913_REG_TUNE, > > + KT0913_TUNE_FMTUNE_ON | (frequency / KT0913_FMCHAN_MUL)); > > +} > > + > > +/* ************************************************************************* */ > > + > > +static int __kt0913_set_mute(struct kt0913_device *radio, int on) > > +{ > > + return regmap_update_bits(radio->regmap, > > + KT0913_REG_VOLUME, KT0913_VOLUME_DMUTE_MASK, > > + on ? KT0913_VOLUME_DMUTE_ON : KT0913_VOLUME_DMUTE_OFF); > > +} > > + > > +/* ************************************************************************* */ > > + > > +static int __kt0913_set_deemphasis(struct kt0913_device *radio, s32 deemp) > > +{ > > + switch (deemp) { > > + case V4L2_DEEMPHASIS_75_uS: > > + return regmap_update_bits(radio->regmap, > > + KT0913_REG_VOLUME, KT0913_VOLUME_DE_MASK, > > + KT0913_VOLUME_DE_50US); > > + > > + /* 50us is used for the disabled option (which is not supported > > + * on the chip) and the 50uS value > > + */ > > + default: > > + return regmap_update_bits(radio->regmap, > > + KT0913_REG_VOLUME, KT0913_VOLUME_DE_MASK, > > + KT0913_VOLUME_DE_75US); > > + } > > +} > > + > > +/* ************************************************************************* */ > > + > > +static int __kt0913_set_volume(struct kt0913_device *radio, s32 volume) > > +{ > > + /* map [-60, 0] to [1, 31] which is what the kt0913 expects */ > > + volume = (volume / 2) + 31; > > + return regmap_update_bits(radio->regmap, > > + KT0913_REG_RXCFG, KT0913_RXCFGA_VOLUME_MASK, > > + volume); > > +} > > + > > +/* ************************************************************************* */ > > + > > +static int __kt0913_set_standby(struct kt0913_device *radio, int standby) > > +{ > > + return regmap_update_bits(radio->regmap, > > + KT0913_REG_RXCFG, KT0913_RXCFGA_STDBY_MASK, > > + standby ? KT0913_RXCFGA_STDBY_ON : KT0913_RXCFGA_STDBY_OFF); > > +} > > + > > +/* ************************************************************************* */ > > + > > +static int __kt0913_get_pll_status(struct kt0913_device *radio, int *locked) > > +{ > > + unsigned int statusa_reg; > > + int ret = regmap_read(radio->regmap, KT0913_REG_STATUSA, &statusa_reg); > > + > > + if (ret) > > + return ret; > > + > > + *locked = (statusa_reg & KT0913_STATUSA_PLL_LOCK_MASK) == > > + KT0913_STATUSA_PLL_LOCK_LOCKED ? 1 : 0; > > + > > + return 0; > > +} > > + > > +/* ************************************************************************* */ > > + > > +static int __kt0913_get_rx_stereo_or_mono(struct kt0913_device *radio, > > + int *stereo) > > +{ > > + unsigned int statusa_reg; > > + int ret = regmap_read(radio->regmap, KT0913_REG_STATUSA, &statusa_reg); > > + > > + if (ret) > > + return ret; > > + > > + *stereo = (statusa_reg & KT0913_STATUSA_ST_MASK) == > > + KT0913_STATUSA_ST_STEREO ? 1 : 0; > > + > > + return 0; > > +} > > + > > +/* ************************************************************************* */ > > + > > +static int __kt0913_get_fm_rssi(struct kt0913_device *radio, s32 *rssi) > > +{ > > + unsigned int statusa_reg; > > + int ret = regmap_read(radio->regmap, KT0913_REG_STATUSA, &statusa_reg); > > + > > + if (ret) > > + return ret; > > + > > + /* RSSI(dBm) = -100 + FMRSSI<4:0> * 3dBm > > + * even tho we can get the value in dBm, we want a % > > + */ > > + *rssi = (statusa_reg & KT0913_STATUSA_FMRSSI_MASK) >> > > + KT0913_STATUSA_FMRSSI_SHIFT; > > + /* map range 0-31 to 0-65535 */ > > + *rssi *= 65535; > > + *rssi /= KT0913_STATUSA_FMRSSI_MASK >> KT0913_STATUSA_FMRSSI_SHIFT; > > + > > + return 0; > > +} > > + > > +/* ************************************************************************* */ > > + > > +static int __kt0913_get_cfg_stereo_enabled(struct kt0913_device *radio, > > + int *stereo) > > +{ > > + unsigned int dspcfga_reg; > > + int ret = regmap_read(radio->regmap, KT0913_REG_DSPCFGA, &dspcfga_reg); > > + > > + if (ret) > > + return ret; > > + > > + *stereo = (dspcfga_reg & KT0913_DSPCFGA_MONO_MASK) == > > + KT0913_DSPCFGA_MONO_OFF ? 1 : 0; > > + > > + return ret; > > +} > > + > > +static int __kt0913_set_cfg_stereo_enabled(struct kt0913_device *radio, > > + int stereo) > > +{ > > + return regmap_update_bits(radio->regmap, > > + KT0913_REG_DSPCFGA, KT0913_DSPCFGA_MONO_MASK, > > + stereo ? KT0913_DSPCFGA_MONO_OFF : KT0913_DSPCFGA_MONO_ON); > > +} > > + > > +/* ************************************************************************* */ > > + > > +static int __kt0913_set_au_gain(struct kt0913_device *radio, s32 gain) > > +{ > > + switch (gain) { > > + case 6: > > + return regmap_update_bits(radio->regmap, > > + KT0913_REG_AMSYSCFG, KT0913_AMSYSCFG_AU_GAIN_MASK, > > + KT0913_AMSYSCFG_AU_GAIN_6DB); > > + case 3: > > + return regmap_update_bits(radio->regmap, > > + KT0913_REG_AMSYSCFG, KT0913_AMSYSCFG_AU_GAIN_MASK, > > + KT0913_AMSYSCFG_AU_GAIN_3DB); > > + case 0: > > + return regmap_update_bits(radio->regmap, > > + KT0913_REG_AMSYSCFG, KT0913_AMSYSCFG_AU_GAIN_MASK, > > + KT0913_AMSYSCFG_AU_GAIN_0DB); > > + case -3: > > + return regmap_update_bits(radio->regmap, > > + KT0913_REG_AMSYSCFG, KT0913_AMSYSCFG_AU_GAIN_MASK, > > + KT0913_AMSYSCFG_AU_GAIN_MIN_3DB); > > + default: > > + return -EINVAL; > > + } > > +} > > + > > +/* ************************************************************************* */ > > + > > +static int __kt0913_set_am_fm_band(struct kt0913_device *radio, > > + unsigned int band) > > +{ > > + return regmap_update_bits(radio->regmap, > > + KT0913_REG_AMSYSCFG, KT0913_AMSYSCFG_AM_FM_MASK, > > + band == BAND_AM ? > > + KT0913_AMSYSCFG_AM_FM_AM : KT0913_AMSYSCFG_AM_FM_FM); > > +} > > + > > +/* ************************************************************************* */ > > + > > +static int __kt0913_get_am_frequency(struct kt0913_device *radio, > > + unsigned int *frequency) > > +{ > > + unsigned int amchan_reg; > > + int ret = regmap_read(radio->regmap, KT0913_REG_AMCHAN, &amchan_reg); > > + > > + if (ret) > > + return ret; > > + > > + *frequency = (amchan_reg & KT0913_AMCHAN_AMCHAN_MASK); > > + > > + return 0; > > +} > > + > > +static int __kt0913_set_am_frequency(struct kt0913_device *radio, > > + unsigned int frequency) > > +{ > > + return regmap_write(radio->regmap, KT0913_REG_AMCHAN, > > + KT0913_AMCHAN_AMTUNE_ON | frequency); > > +} > > + > > +/* ************************************************************************* */ > > + > > +static int __kt0913_get_am_rssi(struct kt0913_device *radio, s32 *rssi) > > +{ > > + unsigned int amstatusa_reg; > > + int ret = regmap_read(radio->regmap, > > + KT0913_REG_AMSTATUSA, &amstatusa_reg); > > + > > + if (ret) > > + return ret; > > + > > + /* AMRSSI(dBm) = -90 + AMRSSI<4:0> * 3dBm > > + * even tho we can get the value in dBm, we want a % > > + */ > > + *rssi = (amstatusa_reg & KT0913_AMSTATUSA_AMRSSI_MASK) >> > > + KT0913_AMSTATUSA_AMRSSI_SHIFT; > > + /* map range 0-31 to 0-65535 */ > > + *rssi *= 65535; > > + *rssi /= KT0913_AMSTATUSA_AMRSSI_MASK >> KT0913_AMSTATUSA_AMRSSI_SHIFT; > > + > > + return 0; > > +} > > + > > +/* ************************************************************************* */ > > + > > +static int __kt0913_init(struct kt0913_device *radio) > > +{ > > + int ret = 0; > > + > > + /* write the defaults */ > > + ret = regmap_multi_reg_write(radio->regmap, > > + kt0913_init_regs_to_defaults, > > + ARRAY_SIZE(kt0913_init_regs_to_defaults)); > > + if (ret) { > > + v4l2_err(radio->client, > > + "regmap_multi_reg_write() failed! %d", ret); > > + return ret; > > + } > > + > > + /* set the audio dac anti-pop config */ > > + ret = regmap_update_bits(radio->regmap, > > + KT0913_REG_VOLUME, KT0913_VOLUME_POP_MASK, > > + radio->audio_anti_pop << > > + KT0913_VOLUME_POP_SHIFT); > > + if (ret) { > > + v4l2_err(radio->client, > > + "regmap_update_bits() err on anti-pop cfg! %d", ret); > > + return ret; > > + } > > + > > + /* set the reference clock config */ > > + ret = regmap_update_bits(radio->regmap, > > + KT0913_REG_AMSYSCFG, > > + KT0913_AMSYSCFG_REFCLK_MASK, > > + radio->refclock_val << > > + KT0913_AMSYSCFG_REFCLK_SHIFT); > > + if (ret) { > > + v4l2_err(radio->client, > > + "regmap_update_bits() err on refclk cfg! %d", ret); > > + return ret; > > + } > > + > > + if (kt0913_use_campus_band) { > > + v4l2_info(radio->client, > > + "campus band is enabled!"); > > + /* set the campus band bit */ > > + ret = regmap_update_bits(radio->regmap, > > + KT0913_REG_LOCFGC, > > + KT0913_LOCFG_CAMPUSBAND_EN_MASK, > > + KT0913_LOCFG_CAMPUSBAND_EN_ON); > > + if (ret) { > > + v4l2_err(radio->client, > > + "regmap_update_bits() err on campus band! %d", > > + ret); > > + return ret; > > + } > > + } > > + > > + return __kt0913_set_mute(radio, true); > > +} > > + > > +/* ************************************************************************* */ > > + > > +static int kt0913_ioctl_vidioc_g_frequency(struct file *file, void *priv, > > + struct v4l2_frequency *f) > > +{ > > + struct kt0913_device *radio = video_drvdata(file); > > + int ret; > > + > > + if (f->tuner != 0) > > + return -EINVAL; > > + > > + f->type = V4L2_TUNER_RADIO; > > + > > + if (radio->band == BAND_AM) > > + ret = __kt0913_get_am_frequency(radio, &f->frequency); > > + else > > + ret = __kt0913_get_fm_frequency(radio, &f->frequency); > > + > > + if (ret) > > + return ret; > > + > > + /* convert kHz freq into v4l2 freq */ > > + f->frequency = khz_to_v4l2_freq(f->frequency); > > + > > + return 0; > > +} > > + > > +static int kt0913_ioctl_vidioc_s_frequency(struct file *file, void *priv, > > + const struct v4l2_frequency *f) > > +{ > > + struct kt0913_device *radio = video_drvdata(file); > > + unsigned int freq = f->frequency; > > + unsigned int new_band; > > + unsigned int half_gap_freq; > > + int ret; > > + > > + if (f->tuner != 0 || f->type != V4L2_TUNER_RADIO) > > + return -EINVAL; > > + > > + if (freq == 0) > > + return -EINVAL; > > + > > + /* calculate the middle frequency on the gap of the bands */ > > + half_gap_freq = kt0913_bands[BAND_AM].rangehigh; > > + > > + if (kt0913_use_campus_band) > > + /* using the AM band and the campus FM band if enabled */ > > + half_gap_freq += kt0913_bands[BAND_FM_CAMUS].rangelow; > > + else > > + /* or using the AM band and the standard FM band */ > > + half_gap_freq += kt0913_bands[BAND_FM].rangelow; > > + > > + half_gap_freq /= 2; > > + > > + if (freq <= half_gap_freq) > > + new_band = BAND_AM; > > + else if (freq <= kt0913_bands[BAND_FM].rangelow && > > + kt0913_use_campus_band) > > + new_band = BAND_FM_CAMUS; > > + else > > + new_band = BAND_FM; > > + > > + /* is the requested band different than the one currently set? */ > > + if (radio->band != new_band) { > > + /* update the band on the device */ > > + ret = __kt0913_set_am_fm_band(radio, new_band); > > + if (ret) > > + return ret; > > + radio->band = new_band; > > + } > > + > > + /* clamp the frequency to the band boundaries */ > > + freq = clamp(freq, kt0913_bands[new_band].rangelow, > > + kt0913_bands[new_band].rangehigh); > > + > > + /* convert v4l2 freq to kHz */ > > + freq = v4l2_freq_to_khz(freq); > > + > > + if (radio->band == BAND_AM) > > + return __kt0913_set_am_frequency(radio, freq); > > + else > > + return __kt0913_set_fm_frequency(radio, freq); > > +} > > + > > +static int kt0913_ioctl_vidioc_enum_freq_bands(struct file *file, void *priv, > > + struct v4l2_frequency_band *band) > > +{ > > + if (band->tuner != 0) > > + return -EINVAL; > > + > > + switch (band->index) { > > + case 0: > > + if (kt0913_use_campus_band) > > + *band = kt0913_bands[BAND_FM_CAMUS]; > > + else > > + *band = kt0913_bands[BAND_FM]; > > + return 0; > > + case 1: > > + *band = kt0913_bands[BAND_AM]; > > + return 0; > > + default: > > + return -EINVAL; > > + } > > +} > > + > > +/* ************************************************************************* */ > > + > > +/* V4L2 vidioc */ > > +static int kt0913_ioctl_vidioc_querycap(struct file *file, void *priv, > > + struct v4l2_capability *capability) > > +{ > > + struct kt0913_device *radio = video_drvdata(file); > > + struct video_device *dev; > > + > > + if (!radio) > > + return -ENODEV; > > + > > + dev = &radio->vdev; > > + > > + if (!dev) > > + return -ENODEV; > > + > > + strscpy(capability->driver, KT0913_FM_AM_DRIVER_NAME, > > + sizeof(capability->driver)); > > + strscpy(capability->card, dev->name, sizeof(capability->card)); > > + snprintf(capability->bus_info, sizeof(capability->bus_info), > > + "I2C:%s", dev_name(&dev->dev)); > > + return 0; > > +} > > + > > +static int kt0913_ioctl_vidioc_g_tuner(struct file *file, void *priv, > > + struct v4l2_tuner *v) > > +{ > > + struct kt0913_device *radio = video_drvdata(file); > > + int ret; > > + int stereo_enabled; > > + int is_stereo; > > + > > + if (v->index != 0) > > + return -EINVAL; > > + > > + strscpy(v->name, "FM/AM", sizeof(v->name)); > > + v->type = V4L2_TUNER_RADIO; > > + > > + v->capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO | > > + V4L2_TUNER_CAP_FREQ_BANDS; > > + > > + v->rangelow = kt0913_bands[BAND_AM].rangelow; > > + v->rangehigh = kt0913_bands[BAND_FM].rangehigh; > > + > > + if (radio->band == BAND_AM) { > > + v->rxsubchans = V4L2_TUNER_SUB_MONO; > > + v->audmode = V4L2_TUNER_MODE_MONO; > > + > > + ret = __kt0913_get_am_rssi(radio, &v->signal); > > + if (ret) > > + return ret; > > + } else { > > + ret = __kt0913_get_cfg_stereo_enabled(radio, &stereo_enabled); > > + if (ret) > > + return ret; > > + > > + v->rxsubchans = stereo_enabled ? > > + V4L2_TUNER_SUB_STEREO : V4L2_TUNER_SUB_MONO; > > + > > + ret = __kt0913_get_rx_stereo_or_mono(radio, &is_stereo); > > + if (ret) > > + return ret; > > + > > + v->audmode = is_stereo ? > > + V4L2_TUNER_MODE_STEREO : V4L2_TUNER_MODE_MONO; > > + > > + ret = __kt0913_get_fm_rssi(radio, &v->signal); > > + if (ret) > > + return ret; > > + } > > + > > + /* AFC is enabled and active by default */ > > + v->afc = 1; > > + > > + return 0; > > +} > > + > > +static int kt0913_ioctl_vidioc_s_tuner(struct file *file, void *priv, > > + const struct v4l2_tuner *v) > > +{ > > + struct kt0913_device *radio = video_drvdata(file); > > + > > + if (v->index != 0) > > + return -EINVAL; > > + > > + /* only mono and stereo are supported */ > > + if (v->audmode != V4L2_TUNER_MODE_MONO && > > + v->audmode != V4L2_TUNER_MODE_STEREO) > > + return 0; > > + > > + /* AM is mono only, so don't try to set it to stereo */ > > + if (radio->band == BAND_AM && v->audmode != V4L2_TUNER_MODE_MONO) > > + return 0; > > + > > + /* set to stereo if specified, otherwise set to mono */ > > + return __kt0913_set_cfg_stereo_enabled(radio, > > + v->audmode == V4L2_TUNER_MODE_STEREO); > > +} > > + > > +static int kt0913_s_ctrl(struct v4l2_ctrl *ctrl) > > +{ > > + struct kt0913_device *radio = v4l2_ctrl_to_device(ctrl); > > + > > + switch (ctrl->id) { > > + case V4L2_CID_AUDIO_MUTE: > > + return __kt0913_set_mute(radio, ctrl->val); > > + case V4L2_CID_AUDIO_VOLUME: > > + return __kt0913_set_volume(radio, ctrl->val); > > + case V4L2_CID_GAIN: > > + return __kt0913_set_au_gain(radio, ctrl->val); > > + case V4L2_CID_TUNE_DEEMPHASIS: > > + return __kt0913_set_deemphasis(radio, ctrl->val); > > + default: > > + return -EINVAL; > > + } > > +} > > + > > +static int kt0913_g_volatile_ctrl(struct v4l2_ctrl *ctrl) > > +{ > > + struct kt0913_device *radio = v4l2_ctrl_to_device(ctrl); > > + > > + switch (ctrl->id) { > > + case V4L2_CID_RF_TUNER_PLL_LOCK: > > + return __kt0913_get_pll_status(radio, &ctrl->val); > > + default: > > + return -EINVAL; > > + } > > +} > > + > > +static const struct v4l2_ctrl_ops kt0913_ctrl_ops = { > > + .s_ctrl = kt0913_s_ctrl, > > + .g_volatile_ctrl = kt0913_g_volatile_ctrl, > > +}; > > + > > +/* ************************************************************************* */ > > + > > +/* File system interface (use the ancillary fops for v4l2) */ > > +static const struct v4l2_file_operations kt0913_radio_fops = { > > + .owner = THIS_MODULE, > > + .open = v4l2_fh_open, > > + .release = v4l2_fh_release, > > + .poll = v4l2_ctrl_poll, > > + .unlocked_ioctl = video_ioctl2, > > +}; > > + > > +/* ioctl ops */ > > +static const struct v4l2_ioctl_ops kt0913_ioctl_ops = { > > + .vidioc_querycap = kt0913_ioctl_vidioc_querycap, > > + .vidioc_g_tuner = kt0913_ioctl_vidioc_g_tuner, > > + .vidioc_s_tuner = kt0913_ioctl_vidioc_s_tuner, > > + .vidioc_g_frequency = kt0913_ioctl_vidioc_g_frequency, > > + .vidioc_s_frequency = kt0913_ioctl_vidioc_s_frequency, > > + .vidioc_enum_freq_bands = kt0913_ioctl_vidioc_enum_freq_bands, > > + /* use ancillary functions for these: */ > > + .vidioc_log_status = v4l2_ctrl_log_status, > > + .vidioc_subscribe_event = v4l2_ctrl_subscribe_event, > > + .vidioc_unsubscribe_event = v4l2_event_unsubscribe, > > +}; > > + > > +/* V4L2 RADIO device structure */ > > +static struct video_device kt0913_radio_template = { > > + .name = KT0913_FM_AM_DRIVER_NAME, > > + .fops = &kt0913_radio_fops, > > + .ioctl_ops = &kt0913_ioctl_ops, > > + .release = video_device_release_empty, > > + .vfl_dir = VFL_DIR_RX, > > + .device_caps = V4L2_CAP_TUNER | V4L2_CAP_RADIO, > > +}; > > + > > +/* ************************************************************************* */ > > + > > +#if IS_ENABLED(CONFIG_OF) > > +static const struct of_device_id kt0913_of_match[] = { > > + { .compatible = "ktm,kt0913" }, > > + { /* sentinel */ } > > +}; > > +MODULE_DEVICE_TABLE(of, kt0913_of_match); > > +#endif /* IS_ENABLED(CONFIG_OF) */ > > + > > +static void __kt0913_parse_dt(struct kt0913_device *radio) > > +{ > > + const void *ptr_anti_pop = of_get_property(radio->client->dev.of_node, > > + "ktm,anti-pop", NULL); > > + const void *ptr_refclk = of_get_property(radio->client->dev.of_node, > > + "ktm,refclk", NULL); > > + > > + if (ptr_anti_pop) { > > + radio->audio_anti_pop = > > + clamp(be32_to_cpup(ptr_anti_pop), 0U, 3U); > > + } else { > > + radio->audio_anti_pop = 0; > > + v4l2_warn(radio->client, > > + "No ktm,anti-pop on dt node, using default"); > > + } > > + > > + if (ptr_refclk) { > > + radio->refclock_val = > > + clamp(be32_to_cpup(ptr_refclk), 0U, 9U); > > + } else { > > + radio->refclock_val = 0; > > + v4l2_warn(radio->client, > > + "No ktm,refclk on dt node, using default"); > > + } > > +} > > + > > +/* ************************************************************************* */ > > + > > +static int kt0913_probe(struct i2c_client *client, > > + const struct i2c_device_id *id) > > +{ > > + struct kt0913_device *radio; > > + struct v4l2_device *v4l2_dev; > > + struct v4l2_ctrl_handler *hdl; > > + struct regmap *regmap; > > + int ret; > > + > > + pr_debug("%s\n", __func__); > > + > > + /* this driver uses word R/W i2c operations, check if it's supported */ > > + ret = i2c_check_functionality(client->adapter, > > + I2C_FUNC_SMBUS_READ_WORD_DATA | > > + I2C_FUNC_SMBUS_WRITE_WORD_DATA); > > + if (!ret) { > > + v4l2_err(client, > > + "I2C adapter doesn't support word operations"); > > + return -EIO; > > + } > > + > > + /* check if the device exist on the bus before initializing it */ > > + ret = i2c_smbus_read_word_data(client, KT0913_REG_CHIP_ID); > > + if (ret < 0) { > > + v4l2_err(client, > > + "Error reading CHIP ID of the kt0913 (%d)", ret); > > + return ret; > > + } > > + > > + /* check if the CHIP ID register value matches the expected value */ > > + if (ret != KT0913_CHIP_ID) { > > + v4l2_err(radio->client, > > + "Invalid CHIP ID: 0x%x, expected 0x%x", > > + ret, KT0913_CHIP_ID); > > + return -ENODEV; > > + } > > + > > + v4l2_info(client, > > + "kt0913 found @ 0x%x (%s)\n", > > + client->addr, client->adapter->name); > > + > > + /* alloc context for the kt0913 radio struct */ > > + radio = devm_kzalloc(&client->dev, sizeof(*radio), GFP_KERNEL); > > + if (!radio) > > + return -ENOMEM; > > + > > + v4l2_dev = &radio->v4l2_dev; > > + ret = v4l2_device_register(&client->dev, v4l2_dev); > > + if (ret < 0) { > > + v4l2_err(client, > > + "could not register v4l2_dev\n"); > > + goto errfr; > > + } > > + > > + mutex_init(&radio->mutex); > > + > > + /* register the control handler from the context struct */ > > + hdl = &radio->ctrl_handler; > > + v4l2_ctrl_handler_init(hdl, 5); > > + > > + /* add the control: Mute */ > > + radio->ctrl_mute = v4l2_ctrl_new_std(hdl, &kt0913_ctrl_ops, > > + V4L2_CID_AUDIO_MUTE, > > + 0, 1, 1, 0); > > + if (hdl->error) { > > + ret = hdl->error; > > + v4l2_err(v4l2_dev, "Could not register control: mute\n"); > > + goto errunreg; > > + } > > + > > + /* add the control: Volume */ > > + radio->ctrl_volume = v4l2_ctrl_new_std(hdl, &kt0913_ctrl_ops, > > + V4L2_CID_AUDIO_VOLUME, > > + -60, 0, 2, 0); > > + if (hdl->error) { > > + ret = hdl->error; > > + v4l2_err(v4l2_dev, "Could not register control: Volume\n"); > > + goto errunreg; > > + } > > + > > + /* add the control: audio gain */ > > + radio->ctrl_au_gain = v4l2_ctrl_new_std(hdl, &kt0913_ctrl_ops, > > + V4L2_CID_GAIN, > > + -3, 6, 3, 3); > > + if (hdl->error) { > > + ret = hdl->error; > > + v4l2_err(v4l2_dev, "Could not register control: audio gain\n"); > > + goto errunreg; > > + } > > + radio->ctrl_au_gain->flags |= V4L2_CTRL_FLAG_SLIDER; > > + > > + /* add the control: PLL Lock */ > > + radio->ctrl_pll_lock = v4l2_ctrl_new_std(hdl, &kt0913_ctrl_ops, > > + V4L2_CID_RF_TUNER_PLL_LOCK, > > + 0, 1, 1, 0); > > + if (hdl->error) { > > + ret = hdl->error; > > + v4l2_err(v4l2_dev, "Could not register control: pll lock\n"); > > + goto errunreg; > > + } > > + radio->ctrl_pll_lock->flags |= (V4L2_CTRL_FLAG_VOLATILE | > > + V4L2_CTRL_FLAG_READ_ONLY); > > + > > + /* add the control: deemphasis */ > > + radio->ctrl_deemp = v4l2_ctrl_new_std_menu(hdl, &kt0913_ctrl_ops, > > + V4L2_CID_TUNE_DEEMPHASIS, > > + V4L2_DEEMPHASIS_75_uS, > > + 0, > > + V4L2_DEEMPHASIS_75_uS); > > + if (hdl->error) { > > + ret = hdl->error; > > + v4l2_err(v4l2_dev, "Could not register control: deemphasis\n"); > > + goto errunreg; > > + } > > + /* the control handler is ready to be used */ > > + v4l2_dev->ctrl_handler = hdl; > > + > > + radio->vdev = kt0913_radio_template; > > + radio->vdev.lock = &radio->mutex; > > + radio->vdev.v4l2_dev = v4l2_dev; > > + video_set_drvdata(&radio->vdev, radio); > > + > > + radio->client = client; > > + i2c_set_clientdata(client, radio); > > + > > + /* init the regmap of the kt0913 */ > > + regmap = devm_regmap_init_i2c(client, &kt0913_regmap_config); > > + if (IS_ERR(regmap)) { > > + ret = PTR_ERR(regmap); > > + v4l2_err(client, > > + "devm_regmap_init_i2c() failed! %d", ret); > > + goto errunreg; > > + } > > + radio->regmap = regmap; > > + > > + __kt0913_parse_dt(radio); > > + > > + /* init the kt0913 into a known state */ > > + ret = __kt0913_init(radio); > > + if (ret) { > > + v4l2_err(client, > > + "__kt0913_init() failed! %d", ret); > > + goto errunreg; > > + } > > + > > + pm_runtime_get_noresume(&client->dev); > > + pm_runtime_set_active(&client->dev); > > + pm_runtime_enable(&client->dev); > > + pm_runtime_dont_use_autosuspend(&client->dev); > > + > > + ret = video_register_device(&radio->vdev, > > + VFL_TYPE_RADIO, kt0913_v4l2_radio_nr); > > + if (ret < 0) { > > + v4l2_err(client, > > + "Could not register video device!"); > > + goto error_pm_disable; > > + } > > + > > + v4l2_info(client, "registered."); > > + return 0; > > +error_pm_disable: > > + pm_runtime_disable(&client->dev); > > + pm_runtime_set_suspended(&client->dev); > > +errunreg: > > + v4l2_ctrl_handler_free(hdl); > > + v4l2_device_unregister(v4l2_dev); > > +errfr: > > + __kt0913_set_standby(radio, true); > > + kfree(radio); > > + return ret; > > +} > > + > > +static int kt0913_remove(struct i2c_client *client) > > +{ > > + struct kt0913_device *radio = i2c_get_clientdata(client); > > + > > + pr_debug("%s\n", __func__); > > + if (!radio) > > + return -EINVAL; > > + > > + __kt0913_set_standby(radio, true); > > + > > + pm_runtime_get_sync(&client->dev); > > + pm_runtime_disable(&client->dev); > > + pm_runtime_set_suspended(&client->dev); > > + pm_runtime_put_noidle(&client->dev); > > + > > + video_unregister_device(&radio->vdev); > > + v4l2_ctrl_handler_free(&radio->ctrl_handler); > > + v4l2_device_unregister(&radio->v4l2_dev); > > + > > + v4l2_info(client, "removed."); > > + return 0; > > +} > > + > > +/* ************************************************************************* */ > > + > > +#ifdef CONFIG_PM > > +static int kt0913_i2c_pm_runtime_suspend(struct device *dev) > > +{ > > + struct kt0913_device *radio = i2c_get_clientdata(to_i2c_client(dev)); > > + > > + pr_debug("%s\n", __func__); > > + if (!radio) > > + return 0; > > + > > + return __kt0913_set_standby(radio, true); > > +} > > + > > +static int kt0913_i2c_pm_runtime_resume(struct device *dev) > > +{ > > + struct kt0913_device *radio = i2c_get_clientdata(to_i2c_client(dev)); > > + > > + pr_debug("%s\n", __func__); > > + if (!radio) > > + return 0; > > + > > + return __kt0913_set_standby(radio, false); > > +} > > +#endif /* CONFIG_PM */ > > + > > +static const struct dev_pm_ops kt0913_i2c_pm_ops = { > > + SET_RUNTIME_PM_OPS(kt0913_i2c_pm_runtime_suspend, > > + kt0913_i2c_pm_runtime_resume, NULL) > > +}; > > + > > +static const struct i2c_device_id kt0913_idtable[] = { > > + { "kt0913", 0 }, > > + { /* sentinel */ } > > +}; > > +MODULE_DEVICE_TABLE(i2c, kt0913_idtable); > > + > > +static struct i2c_driver kt0913_driver = { > > + .driver = { > > + .name = "kt0913", > > + .of_match_table = of_match_ptr(kt0913_of_match), > > + .pm = &kt0913_i2c_pm_ops, > > + }, > > + .probe = kt0913_probe, > > + .remove = kt0913_remove, > > + .id_table = kt0913_idtable, > > +}; > > +module_i2c_driver(kt0913_driver); > > + > > +MODULE_AUTHOR("Santiago Hormazabal <santiagohssl@gmail.com>"); > > +MODULE_DESCRIPTION("KTMicro KT0913 AM/FM receiver"); > > +MODULE_LICENSE("GPL"); > > +MODULE_VERSION("0.0.2"); > > + > > +module_param(kt0913_use_campus_band, int, 0); > > +MODULE_PARM_DESC(kt0913_use_campus_band, "Use the Campus Band feature (FM range 32MHz-110MHz)"); > > +module_param(kt0913_v4l2_radio_nr, int, 0); > > +MODULE_PARM_DESC(kt0913_v4l2_radio_nr, "v4l2 device number to use (i.e. /dev/radioX)"); > > > Thanks, - Santiago H.
diff --git a/drivers/media/radio/Kconfig b/drivers/media/radio/Kconfig index d29e29645e04..ac9053a95f3a 100644 --- a/drivers/media/radio/Kconfig +++ b/drivers/media/radio/Kconfig @@ -226,6 +226,16 @@ config RADIO_WL1273 # TI's ST based wl128x FM radio source "drivers/media/radio/wl128x/Kconfig" +config RADIO_KT0913 + tristate "KT0913 I2C FM/AM radio support" + depends on I2C && VIDEO_V4L2 + help + Say Y here if you want to use the KT0913 FM/AM chip. + This is a low cost chip that uses the I2C bus. + + To compile this driver as a module, choose M here: the + module will be called radio-kt0913. + # # ISA drivers configuration # diff --git a/drivers/media/radio/Makefile b/drivers/media/radio/Makefile index 53c7ae135460..a314121f7771 100644 --- a/drivers/media/radio/Makefile +++ b/drivers/media/radio/Makefile @@ -34,5 +34,6 @@ obj-$(CONFIG_RADIO_WL1273) += radio-wl1273.o obj-$(CONFIG_RADIO_WL128X) += wl128x/ obj-$(CONFIG_RADIO_TEA575X) += tea575x.o obj-$(CONFIG_USB_RAREMONO) += radio-raremono.o +obj-$(CONFIG_RADIO_KT0913) += radio-kt0913.o shark2-objs := radio-shark2.o radio-tea5777.o diff --git a/drivers/media/radio/radio-kt0913.c b/drivers/media/radio/radio-kt0913.c new file mode 100644 index 000000000000..e8c1ff882779 --- /dev/null +++ b/drivers/media/radio/radio-kt0913.c @@ -0,0 +1,1196 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * drivers/media/radio/radio-kt0913.c + * + * Driver for the KT0913 radio chip from KTMicro. + * This driver provides a v4l2 interface to the tuner, using the I2C + * protocol to communicate with the chip. + * It exposes two bands, one for AM and another for FM. If the "campus + * band" feature needs to be enabled, set the corresponding module parameter + * to 1. + * Reference Clock and Audio DAC anti-pop configurations should be + * set via a device tree node. Defaults will be used otherwise. + * + * Audio output should be routed to a speaker or an audio capture + * device. + * + * Based on radio-tea5764 by Fabio Belavenuto <belavenuto@gmail.com> + * + * Copyright (c) 2020 Santiago Hormazabal <santiagohssl@gmail.com> + * + * TODO: + * use rd and wr support for the regmap instead of volatile regs. + * add support for the hardware-assisted frequency seek. + * export FM SNR and AM/FM AFC deviation values as RO controls. + */ + +#include <linux/module.h> +#include <linux/i2c.h> +#include <linux/err.h> +#include <linux/pm_runtime.h> +#include <linux/of.h> +#include <linux/math64.h> +#include <linux/regmap.h> +#include <media/v4l2-ctrls.h> +#include <media/v4l2-device.h> +#include <media/v4l2-ioctl.h> +#include <media/v4l2-event.h> + + /* ************************************************************************* */ + + /* registers of the kt0913 */ +#define KT0913_REG_CHIP_ID 0x01 +#define KT0913_REG_SEEK 0x02 +#define KT0913_REG_TUNE 0x03 +#define KT0913_REG_VOLUME 0x04 +#define KT0913_REG_DSPCFGA 0x05 +#define KT0913_REG_LOCFGA 0x0A +#define KT0913_REG_LOCFGC 0x0C +#define KT0913_REG_RXCFG 0x0F +#define KT0913_REG_STATUSA 0x12 +#define KT0913_REG_STATUSB 0x13 +#define KT0913_REG_STATUSC 0x14 +#define KT0913_REG_AMSYSCFG 0x16 +#define KT0913_REG_AMCHAN 0x17 +#define KT0913_REG_AMCALI 0x18 +#define KT0913_REG_GPIOCFG 0x1D +#define KT0913_REG_AMDSP 0x22 +#define KT0913_REG_AMSTATUSA 0x24 +#define KT0913_REG_AMSTATUSB 0x25 +#define KT0913_REG_SOFTMUTE 0x2E +#define KT0913_REG_AMCFG 0x33 +#define KT0913_REG_AMCFG2 0x34 +#define KT0913_REG_AFC 0x3C + +/* register symbols masks, values and shift count */ +#define KT0913_TUNE_FMTUNE_MASK 0x8000 /* FM Tune enable */ +#define KT0913_TUNE_FMTUNE_ON 0x8000 /* FM Tune enabled */ +#define KT0913_TUNE_FMTUNE_OFF 0x0000 /* FM Tune disabled */ +#define KT0913_TUNE_FMCHAN_MASK 0x0FFF /* frequency in kHz / 50kHz */ + +#define KT0913_VOLUME_DMUTE_MASK 0x2000 +#define KT0913_VOLUME_DMUTE_ON 0x0000 +#define KT0913_VOLUME_DMUTE_OFF 0x2000 +#define KT0913_VOLUME_DE_MASK 0x0800 /* de-emphasis time constant */ +#define KT0913_VOLUME_DE_75US 0x0000 /* 75us */ +#define KT0913_VOLUME_DE_50US 0x0800 /* 50us */ +#define KT0913_VOLUME_POP_MASK 0x30 /* audio dac anti-pop config */ +#define KT0913_VOLUME_POP_SHIFT 4 + +#define KT0913_DSPCFGA_MONO_MASK 0x8000 /* mono select (0=stereo, 1=mono) */ +#define KT0913_DSPCFGA_MONO_ON 0x8000 /* mono */ +#define KT0913_DSPCFGA_MONO_OFF 0x0000 /* stereo */ + +#define KT0913_LOCFG_CAMPUSBAND_EN_MASK 0x0008 /* campus band fm enable */ +#define KT0913_LOCFG_CAMPUSBAND_EN_ON 0x0008 /* FM range 64-110MHz */ +#define KT0913_LOCFG_CAMPUSBAND_EN_OFF 0x0000 /* FM range 32-110MHz */ + +#define KT0913_RXCFGA_STDBY_MASK 0x1000 /* standby mode enable */ +#define KT0913_RXCFGA_STDBY_ON 0x1000 /* standby mode enabled */ +#define KT0913_RXCFGA_STDBY_OFF 0x0000 /* standby mode disabled */ +#define KT0913_RXCFGA_VOLUME_MASK 0x001F /* volume control */ + +#define KT0913_STATUSA_XTAL_OK 0x8000 /* crystal ready indicator */ +#define KT0913_STATUSA_STC 0x4000 /* seek/tune complete */ + +#define KT0913_STATUSA_PLL_LOCK_MASK 0x800 /* system pll ready indicator */ +#define KT0913_STATUSA_PLL_LOCK_LOCKED 0x800 /* system pll ready */ +#define KT0913_STATUSA_PLL_LOCK_UNLOCKED 0x000 /* not ready */ +#define KT0913_STATUSA_LO_LOCK 0x400 /* LO synthesizer ready indicator */ +#define KT0913_STATUSA_ST_MASK 0x300 /* stereo indicator (0x300=stereo, otherwise mono) */ +#define KT0913_STATUSA_ST_STEREO 0x300 /* stereo */ +#define KT0913_STATUSA_FMRSSI_MASK 0xF8 /* FM RSSI (-100dBm + FMRSSI*3dBm) */ +#define KT0913_STATUSA_FMRSSI_SHIFT 3 + +#define KT0913_STATUSC_PWSTATUS 0x8000 /* power status indicator */ +#define KT0913_STATUSC_CHIPRDY 0x2000 /* chip ready indicator */ +#define KT0913_STATUSC_FMSNR 0x1FC0 /* FM SNR (unknown units) */ + +#define KT0913_AMCHAN_AMTUNE_MASK 0x8000 /* AM tune enable */ +#define KT0913_AMCHAN_AMTUNE_ON 0x8000 /* AM tune enabled */ +#define KT0913_AMCHAN_AMTUNE_OFF 0x0000 /* AM tune disabled */ +#define KT0913_AMCHAN_AMCHAN_MASK 0x7FF /* am channel in kHz */ + +#define KT0913_AMSYSCFG_AM_FM_MASK 0x8000 /* am/fm mode control */ +#define KT0913_AMSYSCFG_AM_FM_AM 0x8000 /* am mode */ +#define KT0913_AMSYSCFG_AM_FM_FM 0x0000 /* fm mode (default) */ +#define KT0913_AMSYSCFG_REFCLK_MASK 0x0F00 /* reference clock selection */ +#define KT0913_AMSYSCFG_REFCLK_SHIFT 8 +#define KT0913_AMSYSCFG_AU_GAIN_MASK 0x00C0 /* audio gain selection */ +#define KT0913_AMSYSCFG_AU_GAIN_6DB 0x0040 /* 6dB audio gain */ +#define KT0913_AMSYSCFG_AU_GAIN_3DB 0x0000 /* 3dB audio gain (default) */ +#define KT0913_AMSYSCFG_AU_GAIN_0DB 0x00C0 /* 0dB audio gain */ +#define KT0913_AMSYSCFG_AU_GAIN_MIN_3DB 0x0080 /* -3dB audio gain */ + +#define KT0913_AMSTATUSA_AMRSSI_MASK 0x1F00 /* am channel rssi */ +#define KT0913_AMSTATUSA_AMRSSI_SHIFT 8 + +/* constants */ +#define KT0913_CHIP_ID 0x544B /* ASCII of 'KT' */ + +#define V4L2_KHZ_FREQ_MUL 16U /* v4l2 uses 16x the kHz value as their freq */ +#define KT0913_FMCHAN_MUL 50U /* kt0913 uses freqs with a 50kHz multiplier */ +#define KT0913_FM_RANGE_LOW_NO_CAMPUS 64000U /* 64MHz lower bound for FM */ +#define KT0913_FM_RANGE_LOW_CAMPUS 32000U /* 32MHz lower bound for campus FM */ +#define KT0913_FM_RANGE_HIGH 110000U /* 110MHz upper bound for FM */ +#define KT0913_AM_RANGE_LOW 500U /* 500kHz lower bound for AM */ +#define KT0913_AM_RANGE_HIGH 1710U /* 1710kHz upper bound for AM */ + +#define KT0913_FM_AM_DRIVER_NAME "kt0913-fm-am" + +/* ************************************************************************* */ + +/* v4l2 device number to use. -1 will assign the next free one */ +static int kt0913_v4l2_radio_nr = -1; +/* use the extended range of FM down to 32MHz. disabled by default */ +static int kt0913_use_campus_band; + +/* ************************************************************************* */ + +/* kt0913 status struct */ +struct kt0913_device { + struct v4l2_device v4l2_dev; /* main v4l2 struct */ + struct i2c_client *client; /* I2C client */ + struct video_device vdev; /* vide_device struct */ + struct v4l2_ctrl_handler ctrl_handler; /* ctrl_handler struct */ + + /* V4L2 Controls */ + struct v4l2_ctrl *ctrl_pll_lock; /* PLL lock */ + struct v4l2_ctrl *ctrl_volume; /* Overall volume */ + struct v4l2_ctrl *ctrl_au_gain; /* Audio Gain */ + struct v4l2_ctrl *ctrl_mute; /* Master mute */ + struct v4l2_ctrl *ctrl_deemp; /* Deemphasis */ + + /* current operation band (fm, fm_campus, am) */ + unsigned int band; + + /* audio dac anti-pop setting: + * 0 -> 100uF (default) + * 1 -> 60uF + * 2 -> 20uF + * 3 -> 10uF + */ + unsigned int audio_anti_pop; + + /* + * reference clock selection: + * 0 -> 32.768kHz (default) + * 1 -> 6.5MHz + * 2 -> 7.6MHz + * 3 -> 12MHz + * 4 -> 13MHz + * 5 -> 15.2MHz + * 6 -> 19.2MHz + * 7 -> 24MHz + * 8 -> 26MHz + * 9 -> 38kHz + */ + unsigned int refclock_val; + + /* Regmap */ + struct regmap *regmap; + + /* For core assisted locking */ + struct mutex mutex; +}; + +/* ************************************************************************* */ + +/* Regmap settings */ +static const struct regmap_range kt0913_regmap_all_registers_range[] = { + regmap_reg_range(0x01, 0x05), + regmap_reg_range(0x0A, 0x0A), + regmap_reg_range(0x0C, 0x0C), + regmap_reg_range(0x0F, 0x0F), + regmap_reg_range(0x12, 0x14), + regmap_reg_range(0x16, 0x18), + regmap_reg_range(0x1D, 0x1D), + regmap_reg_range(0x22, 0x22), + regmap_reg_range(0x24, 0x25), + regmap_reg_range(0x2E, 0x2F), + regmap_reg_range(0x30, 0x34), + regmap_reg_range(0x3A, 0x3A), + regmap_reg_range(0x3C, 0x3C), +}; + +static const struct regmap_access_table kt0913_all_registers_access_table = { + .yes_ranges = kt0913_regmap_all_registers_range, + .n_yes_ranges = ARRAY_SIZE(kt0913_regmap_all_registers_range), +}; + +static const struct reg_sequence kt0913_init_regs_to_defaults[] = { + /* Standby disabled, volume 0dB */ + { KT0913_REG_RXCFG, 0x881F }, + /* FM Channel spacing = 50kHz, Right & Left unmuted */ + { KT0913_REG_SEEK, 0x000B }, + /* Stereo, High Stereo/Mono blend level, blend disabled */ + { KT0913_REG_DSPCFGA, 0x1000 }, + /* FM AFC Enabled */ + { KT0913_REG_LOCFGA, 0x0100 }, + /* Campus band disabled by default */ + { KT0913_REG_LOCFGC, 0x0024 }, + /* + * FM mode, internal defined bands, clock from XT, 32.768kHz + * 3dB audio gain, AM AFC Enabled + */ + { KT0913_REG_AMSYSCFG, 0x0002 }, + /* Default AM freq = 504kHz */ + { KT0913_REG_AMCHAN, 0x01F8}, + /* VOL and CH GPIOs set to HiZ */ + { KT0913_REG_GPIOCFG, 0x0000 }, + /* AM Channel bandwidth = 6kHz, non-differential output */ + { KT0913_REG_AMDSP, 0xAFC4 }, + /* + * softmute is disabled on AM and FM, but set the defaults: + * strong softmute attn., slow softmute attack/recover, + * lowest AM softumte start level, almost the minimum + * softmute target volume, RSSI mode for softmute, lowest + * FM softmute start level + */ + { KT0913_REG_SOFTMUTE, 0x0010 }, + /* 1kHz for AM channel space, working mode A for the keys */ + { KT0913_REG_AMCFG, 0x1401 }, + /* TIME1 = shortest, TIME2 = fastest */ + { KT0913_REG_AMCFG, 0x4050 }, + /* set 86MHz as the default frequency, and tune it */ + { KT0913_REG_TUNE, 0x86B8 }, + /* + * FM&AM Softmute disabled, Mute disabled, 75us deemp., + * no bass boost, 100uF anti pop cap + */ + { KT0913_REG_VOLUME, 0xE080 }, +}; + +static const struct regmap_config kt0913_regmap_config = { + .reg_bits = 8, + .val_bits = 16, + .max_register = KT0913_REG_AFC, + .volatile_table = &kt0913_all_registers_access_table, + .cache_type = REGCACHE_RBTREE, + .val_format_endian = REGMAP_ENDIAN_BIG, +}; + +/* ************************************************************************* */ + +/* bands where the kt0913 operates */ +enum { BAND_FM, BAND_FM_CAMUS, BAND_AM }; + +static const struct v4l2_frequency_band kt0913_bands[] = { + { + /* BAND_FM */ + .type = V4L2_TUNER_RADIO, + .index = 0, /* index provided to v4l2 */ + .capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO | + V4L2_TUNER_CAP_FREQ_BANDS, + .rangelow = KT0913_FM_RANGE_LOW_NO_CAMPUS * V4L2_KHZ_FREQ_MUL, + .rangehigh = KT0913_FM_RANGE_HIGH * V4L2_KHZ_FREQ_MUL, + .modulation = V4L2_BAND_MODULATION_FM, + }, + { + /* BAND_FM_CAMUS */ + .type = V4L2_TUNER_RADIO, + .index = 0, /* index provided to v4l2 */ + .capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO | + V4L2_TUNER_CAP_FREQ_BANDS, + .rangelow = KT0913_FM_RANGE_LOW_CAMPUS * V4L2_KHZ_FREQ_MUL, + .rangehigh = KT0913_FM_RANGE_HIGH * V4L2_KHZ_FREQ_MUL, + .modulation = V4L2_BAND_MODULATION_FM, + }, + { + /* BAND_AM */ + .type = V4L2_TUNER_RADIO, + .index = 1, /* index provided to v4l2 */ + .capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_FREQ_BANDS, + .rangelow = KT0913_AM_RANGE_LOW * V4L2_KHZ_FREQ_MUL, + .rangehigh = KT0913_AM_RANGE_HIGH * V4L2_KHZ_FREQ_MUL, + .modulation = V4L2_BAND_MODULATION_AM, + }, +}; + +/* ************************************************************************* */ + +static inline struct kt0913_device * +v4l2_device_to_device(struct v4l2_device *v4l2_dev) +{ + return container_of(v4l2_dev, + struct kt0913_device, v4l2_dev); +} + +static inline struct kt0913_device * +v4l2_ctrl_to_device(struct v4l2_ctrl *ctrl_handler) +{ + return container_of(ctrl_handler->handler, + struct kt0913_device, ctrl_handler); +} + +/* ************************************************************************* */ + +static inline u32 khz_to_v4l2_freq(unsigned int freq) +{ + return freq * V4L2_KHZ_FREQ_MUL; +} + +static inline unsigned int v4l2_freq_to_khz(u32 v4l2_freq) +{ + return v4l2_freq / V4L2_KHZ_FREQ_MUL; +} + +/* ************************************************************************* */ + +static int __kt0913_get_fm_frequency(struct kt0913_device *radio, + unsigned int *frequency) +{ + unsigned int tune_reg; + int ret = regmap_read(radio->regmap, KT0913_REG_TUNE, &tune_reg); + + if (ret) + return ret; + + *frequency = (tune_reg & KT0913_TUNE_FMCHAN_MASK) * KT0913_FMCHAN_MUL; + + return 0; +} + +static int __kt0913_set_fm_frequency(struct kt0913_device *radio, + unsigned int frequency) +{ + return regmap_write(radio->regmap, KT0913_REG_TUNE, + KT0913_TUNE_FMTUNE_ON | (frequency / KT0913_FMCHAN_MUL)); +} + +/* ************************************************************************* */ + +static int __kt0913_set_mute(struct kt0913_device *radio, int on) +{ + return regmap_update_bits(radio->regmap, + KT0913_REG_VOLUME, KT0913_VOLUME_DMUTE_MASK, + on ? KT0913_VOLUME_DMUTE_ON : KT0913_VOLUME_DMUTE_OFF); +} + +/* ************************************************************************* */ + +static int __kt0913_set_deemphasis(struct kt0913_device *radio, s32 deemp) +{ + switch (deemp) { + case V4L2_DEEMPHASIS_75_uS: + return regmap_update_bits(radio->regmap, + KT0913_REG_VOLUME, KT0913_VOLUME_DE_MASK, + KT0913_VOLUME_DE_50US); + + /* 50us is used for the disabled option (which is not supported + * on the chip) and the 50uS value + */ + default: + return regmap_update_bits(radio->regmap, + KT0913_REG_VOLUME, KT0913_VOLUME_DE_MASK, + KT0913_VOLUME_DE_75US); + } +} + +/* ************************************************************************* */ + +static int __kt0913_set_volume(struct kt0913_device *radio, s32 volume) +{ + /* map [-60, 0] to [1, 31] which is what the kt0913 expects */ + volume = (volume / 2) + 31; + return regmap_update_bits(radio->regmap, + KT0913_REG_RXCFG, KT0913_RXCFGA_VOLUME_MASK, + volume); +} + +/* ************************************************************************* */ + +static int __kt0913_set_standby(struct kt0913_device *radio, int standby) +{ + return regmap_update_bits(radio->regmap, + KT0913_REG_RXCFG, KT0913_RXCFGA_STDBY_MASK, + standby ? KT0913_RXCFGA_STDBY_ON : KT0913_RXCFGA_STDBY_OFF); +} + +/* ************************************************************************* */ + +static int __kt0913_get_pll_status(struct kt0913_device *radio, int *locked) +{ + unsigned int statusa_reg; + int ret = regmap_read(radio->regmap, KT0913_REG_STATUSA, &statusa_reg); + + if (ret) + return ret; + + *locked = (statusa_reg & KT0913_STATUSA_PLL_LOCK_MASK) == + KT0913_STATUSA_PLL_LOCK_LOCKED ? 1 : 0; + + return 0; +} + +/* ************************************************************************* */ + +static int __kt0913_get_rx_stereo_or_mono(struct kt0913_device *radio, + int *stereo) +{ + unsigned int statusa_reg; + int ret = regmap_read(radio->regmap, KT0913_REG_STATUSA, &statusa_reg); + + if (ret) + return ret; + + *stereo = (statusa_reg & KT0913_STATUSA_ST_MASK) == + KT0913_STATUSA_ST_STEREO ? 1 : 0; + + return 0; +} + +/* ************************************************************************* */ + +static int __kt0913_get_fm_rssi(struct kt0913_device *radio, s32 *rssi) +{ + unsigned int statusa_reg; + int ret = regmap_read(radio->regmap, KT0913_REG_STATUSA, &statusa_reg); + + if (ret) + return ret; + + /* RSSI(dBm) = -100 + FMRSSI<4:0> * 3dBm + * even tho we can get the value in dBm, we want a % + */ + *rssi = (statusa_reg & KT0913_STATUSA_FMRSSI_MASK) >> + KT0913_STATUSA_FMRSSI_SHIFT; + /* map range 0-31 to 0-65535 */ + *rssi *= 65535; + *rssi /= KT0913_STATUSA_FMRSSI_MASK >> KT0913_STATUSA_FMRSSI_SHIFT; + + return 0; +} + +/* ************************************************************************* */ + +static int __kt0913_get_cfg_stereo_enabled(struct kt0913_device *radio, + int *stereo) +{ + unsigned int dspcfga_reg; + int ret = regmap_read(radio->regmap, KT0913_REG_DSPCFGA, &dspcfga_reg); + + if (ret) + return ret; + + *stereo = (dspcfga_reg & KT0913_DSPCFGA_MONO_MASK) == + KT0913_DSPCFGA_MONO_OFF ? 1 : 0; + + return ret; +} + +static int __kt0913_set_cfg_stereo_enabled(struct kt0913_device *radio, + int stereo) +{ + return regmap_update_bits(radio->regmap, + KT0913_REG_DSPCFGA, KT0913_DSPCFGA_MONO_MASK, + stereo ? KT0913_DSPCFGA_MONO_OFF : KT0913_DSPCFGA_MONO_ON); +} + +/* ************************************************************************* */ + +static int __kt0913_set_au_gain(struct kt0913_device *radio, s32 gain) +{ + switch (gain) { + case 6: + return regmap_update_bits(radio->regmap, + KT0913_REG_AMSYSCFG, KT0913_AMSYSCFG_AU_GAIN_MASK, + KT0913_AMSYSCFG_AU_GAIN_6DB); + case 3: + return regmap_update_bits(radio->regmap, + KT0913_REG_AMSYSCFG, KT0913_AMSYSCFG_AU_GAIN_MASK, + KT0913_AMSYSCFG_AU_GAIN_3DB); + case 0: + return regmap_update_bits(radio->regmap, + KT0913_REG_AMSYSCFG, KT0913_AMSYSCFG_AU_GAIN_MASK, + KT0913_AMSYSCFG_AU_GAIN_0DB); + case -3: + return regmap_update_bits(radio->regmap, + KT0913_REG_AMSYSCFG, KT0913_AMSYSCFG_AU_GAIN_MASK, + KT0913_AMSYSCFG_AU_GAIN_MIN_3DB); + default: + return -EINVAL; + } +} + +/* ************************************************************************* */ + +static int __kt0913_set_am_fm_band(struct kt0913_device *radio, + unsigned int band) +{ + return regmap_update_bits(radio->regmap, + KT0913_REG_AMSYSCFG, KT0913_AMSYSCFG_AM_FM_MASK, + band == BAND_AM ? + KT0913_AMSYSCFG_AM_FM_AM : KT0913_AMSYSCFG_AM_FM_FM); +} + +/* ************************************************************************* */ + +static int __kt0913_get_am_frequency(struct kt0913_device *radio, + unsigned int *frequency) +{ + unsigned int amchan_reg; + int ret = regmap_read(radio->regmap, KT0913_REG_AMCHAN, &amchan_reg); + + if (ret) + return ret; + + *frequency = (amchan_reg & KT0913_AMCHAN_AMCHAN_MASK); + + return 0; +} + +static int __kt0913_set_am_frequency(struct kt0913_device *radio, + unsigned int frequency) +{ + return regmap_write(radio->regmap, KT0913_REG_AMCHAN, + KT0913_AMCHAN_AMTUNE_ON | frequency); +} + +/* ************************************************************************* */ + +static int __kt0913_get_am_rssi(struct kt0913_device *radio, s32 *rssi) +{ + unsigned int amstatusa_reg; + int ret = regmap_read(radio->regmap, + KT0913_REG_AMSTATUSA, &amstatusa_reg); + + if (ret) + return ret; + + /* AMRSSI(dBm) = -90 + AMRSSI<4:0> * 3dBm + * even tho we can get the value in dBm, we want a % + */ + *rssi = (amstatusa_reg & KT0913_AMSTATUSA_AMRSSI_MASK) >> + KT0913_AMSTATUSA_AMRSSI_SHIFT; + /* map range 0-31 to 0-65535 */ + *rssi *= 65535; + *rssi /= KT0913_AMSTATUSA_AMRSSI_MASK >> KT0913_AMSTATUSA_AMRSSI_SHIFT; + + return 0; +} + +/* ************************************************************************* */ + +static int __kt0913_init(struct kt0913_device *radio) +{ + int ret = 0; + + /* write the defaults */ + ret = regmap_multi_reg_write(radio->regmap, + kt0913_init_regs_to_defaults, + ARRAY_SIZE(kt0913_init_regs_to_defaults)); + if (ret) { + v4l2_err(radio->client, + "regmap_multi_reg_write() failed! %d", ret); + return ret; + } + + /* set the audio dac anti-pop config */ + ret = regmap_update_bits(radio->regmap, + KT0913_REG_VOLUME, KT0913_VOLUME_POP_MASK, + radio->audio_anti_pop << + KT0913_VOLUME_POP_SHIFT); + if (ret) { + v4l2_err(radio->client, + "regmap_update_bits() err on anti-pop cfg! %d", ret); + return ret; + } + + /* set the reference clock config */ + ret = regmap_update_bits(radio->regmap, + KT0913_REG_AMSYSCFG, + KT0913_AMSYSCFG_REFCLK_MASK, + radio->refclock_val << + KT0913_AMSYSCFG_REFCLK_SHIFT); + if (ret) { + v4l2_err(radio->client, + "regmap_update_bits() err on refclk cfg! %d", ret); + return ret; + } + + if (kt0913_use_campus_band) { + v4l2_info(radio->client, + "campus band is enabled!"); + /* set the campus band bit */ + ret = regmap_update_bits(radio->regmap, + KT0913_REG_LOCFGC, + KT0913_LOCFG_CAMPUSBAND_EN_MASK, + KT0913_LOCFG_CAMPUSBAND_EN_ON); + if (ret) { + v4l2_err(radio->client, + "regmap_update_bits() err on campus band! %d", + ret); + return ret; + } + } + + return __kt0913_set_mute(radio, true); +} + +/* ************************************************************************* */ + +static int kt0913_ioctl_vidioc_g_frequency(struct file *file, void *priv, + struct v4l2_frequency *f) +{ + struct kt0913_device *radio = video_drvdata(file); + int ret; + + if (f->tuner != 0) + return -EINVAL; + + f->type = V4L2_TUNER_RADIO; + + if (radio->band == BAND_AM) + ret = __kt0913_get_am_frequency(radio, &f->frequency); + else + ret = __kt0913_get_fm_frequency(radio, &f->frequency); + + if (ret) + return ret; + + /* convert kHz freq into v4l2 freq */ + f->frequency = khz_to_v4l2_freq(f->frequency); + + return 0; +} + +static int kt0913_ioctl_vidioc_s_frequency(struct file *file, void *priv, + const struct v4l2_frequency *f) +{ + struct kt0913_device *radio = video_drvdata(file); + unsigned int freq = f->frequency; + unsigned int new_band; + unsigned int half_gap_freq; + int ret; + + if (f->tuner != 0 || f->type != V4L2_TUNER_RADIO) + return -EINVAL; + + if (freq == 0) + return -EINVAL; + + /* calculate the middle frequency on the gap of the bands */ + half_gap_freq = kt0913_bands[BAND_AM].rangehigh; + + if (kt0913_use_campus_band) + /* using the AM band and the campus FM band if enabled */ + half_gap_freq += kt0913_bands[BAND_FM_CAMUS].rangelow; + else + /* or using the AM band and the standard FM band */ + half_gap_freq += kt0913_bands[BAND_FM].rangelow; + + half_gap_freq /= 2; + + if (freq <= half_gap_freq) + new_band = BAND_AM; + else if (freq <= kt0913_bands[BAND_FM].rangelow && + kt0913_use_campus_band) + new_band = BAND_FM_CAMUS; + else + new_band = BAND_FM; + + /* is the requested band different than the one currently set? */ + if (radio->band != new_band) { + /* update the band on the device */ + ret = __kt0913_set_am_fm_band(radio, new_band); + if (ret) + return ret; + radio->band = new_band; + } + + /* clamp the frequency to the band boundaries */ + freq = clamp(freq, kt0913_bands[new_band].rangelow, + kt0913_bands[new_band].rangehigh); + + /* convert v4l2 freq to kHz */ + freq = v4l2_freq_to_khz(freq); + + if (radio->band == BAND_AM) + return __kt0913_set_am_frequency(radio, freq); + else + return __kt0913_set_fm_frequency(radio, freq); +} + +static int kt0913_ioctl_vidioc_enum_freq_bands(struct file *file, void *priv, + struct v4l2_frequency_band *band) +{ + if (band->tuner != 0) + return -EINVAL; + + switch (band->index) { + case 0: + if (kt0913_use_campus_band) + *band = kt0913_bands[BAND_FM_CAMUS]; + else + *band = kt0913_bands[BAND_FM]; + return 0; + case 1: + *band = kt0913_bands[BAND_AM]; + return 0; + default: + return -EINVAL; + } +} + +/* ************************************************************************* */ + +/* V4L2 vidioc */ +static int kt0913_ioctl_vidioc_querycap(struct file *file, void *priv, + struct v4l2_capability *capability) +{ + struct kt0913_device *radio = video_drvdata(file); + struct video_device *dev; + + if (!radio) + return -ENODEV; + + dev = &radio->vdev; + + if (!dev) + return -ENODEV; + + strscpy(capability->driver, KT0913_FM_AM_DRIVER_NAME, + sizeof(capability->driver)); + strscpy(capability->card, dev->name, sizeof(capability->card)); + snprintf(capability->bus_info, sizeof(capability->bus_info), + "I2C:%s", dev_name(&dev->dev)); + return 0; +} + +static int kt0913_ioctl_vidioc_g_tuner(struct file *file, void *priv, + struct v4l2_tuner *v) +{ + struct kt0913_device *radio = video_drvdata(file); + int ret; + int stereo_enabled; + int is_stereo; + + if (v->index != 0) + return -EINVAL; + + strscpy(v->name, "FM/AM", sizeof(v->name)); + v->type = V4L2_TUNER_RADIO; + + v->capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO | + V4L2_TUNER_CAP_FREQ_BANDS; + + v->rangelow = kt0913_bands[BAND_AM].rangelow; + v->rangehigh = kt0913_bands[BAND_FM].rangehigh; + + if (radio->band == BAND_AM) { + v->rxsubchans = V4L2_TUNER_SUB_MONO; + v->audmode = V4L2_TUNER_MODE_MONO; + + ret = __kt0913_get_am_rssi(radio, &v->signal); + if (ret) + return ret; + } else { + ret = __kt0913_get_cfg_stereo_enabled(radio, &stereo_enabled); + if (ret) + return ret; + + v->rxsubchans = stereo_enabled ? + V4L2_TUNER_SUB_STEREO : V4L2_TUNER_SUB_MONO; + + ret = __kt0913_get_rx_stereo_or_mono(radio, &is_stereo); + if (ret) + return ret; + + v->audmode = is_stereo ? + V4L2_TUNER_MODE_STEREO : V4L2_TUNER_MODE_MONO; + + ret = __kt0913_get_fm_rssi(radio, &v->signal); + if (ret) + return ret; + } + + /* AFC is enabled and active by default */ + v->afc = 1; + + return 0; +} + +static int kt0913_ioctl_vidioc_s_tuner(struct file *file, void *priv, + const struct v4l2_tuner *v) +{ + struct kt0913_device *radio = video_drvdata(file); + + if (v->index != 0) + return -EINVAL; + + /* only mono and stereo are supported */ + if (v->audmode != V4L2_TUNER_MODE_MONO && + v->audmode != V4L2_TUNER_MODE_STEREO) + return 0; + + /* AM is mono only, so don't try to set it to stereo */ + if (radio->band == BAND_AM && v->audmode != V4L2_TUNER_MODE_MONO) + return 0; + + /* set to stereo if specified, otherwise set to mono */ + return __kt0913_set_cfg_stereo_enabled(radio, + v->audmode == V4L2_TUNER_MODE_STEREO); +} + +static int kt0913_s_ctrl(struct v4l2_ctrl *ctrl) +{ + struct kt0913_device *radio = v4l2_ctrl_to_device(ctrl); + + switch (ctrl->id) { + case V4L2_CID_AUDIO_MUTE: + return __kt0913_set_mute(radio, ctrl->val); + case V4L2_CID_AUDIO_VOLUME: + return __kt0913_set_volume(radio, ctrl->val); + case V4L2_CID_GAIN: + return __kt0913_set_au_gain(radio, ctrl->val); + case V4L2_CID_TUNE_DEEMPHASIS: + return __kt0913_set_deemphasis(radio, ctrl->val); + default: + return -EINVAL; + } +} + +static int kt0913_g_volatile_ctrl(struct v4l2_ctrl *ctrl) +{ + struct kt0913_device *radio = v4l2_ctrl_to_device(ctrl); + + switch (ctrl->id) { + case V4L2_CID_RF_TUNER_PLL_LOCK: + return __kt0913_get_pll_status(radio, &ctrl->val); + default: + return -EINVAL; + } +} + +static const struct v4l2_ctrl_ops kt0913_ctrl_ops = { + .s_ctrl = kt0913_s_ctrl, + .g_volatile_ctrl = kt0913_g_volatile_ctrl, +}; + +/* ************************************************************************* */ + +/* File system interface (use the ancillary fops for v4l2) */ +static const struct v4l2_file_operations kt0913_radio_fops = { + .owner = THIS_MODULE, + .open = v4l2_fh_open, + .release = v4l2_fh_release, + .poll = v4l2_ctrl_poll, + .unlocked_ioctl = video_ioctl2, +}; + +/* ioctl ops */ +static const struct v4l2_ioctl_ops kt0913_ioctl_ops = { + .vidioc_querycap = kt0913_ioctl_vidioc_querycap, + .vidioc_g_tuner = kt0913_ioctl_vidioc_g_tuner, + .vidioc_s_tuner = kt0913_ioctl_vidioc_s_tuner, + .vidioc_g_frequency = kt0913_ioctl_vidioc_g_frequency, + .vidioc_s_frequency = kt0913_ioctl_vidioc_s_frequency, + .vidioc_enum_freq_bands = kt0913_ioctl_vidioc_enum_freq_bands, + /* use ancillary functions for these: */ + .vidioc_log_status = v4l2_ctrl_log_status, + .vidioc_subscribe_event = v4l2_ctrl_subscribe_event, + .vidioc_unsubscribe_event = v4l2_event_unsubscribe, +}; + +/* V4L2 RADIO device structure */ +static struct video_device kt0913_radio_template = { + .name = KT0913_FM_AM_DRIVER_NAME, + .fops = &kt0913_radio_fops, + .ioctl_ops = &kt0913_ioctl_ops, + .release = video_device_release_empty, + .vfl_dir = VFL_DIR_RX, + .device_caps = V4L2_CAP_TUNER | V4L2_CAP_RADIO, +}; + +/* ************************************************************************* */ + +#if IS_ENABLED(CONFIG_OF) +static const struct of_device_id kt0913_of_match[] = { + { .compatible = "ktm,kt0913" }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, kt0913_of_match); +#endif /* IS_ENABLED(CONFIG_OF) */ + +static void __kt0913_parse_dt(struct kt0913_device *radio) +{ + const void *ptr_anti_pop = of_get_property(radio->client->dev.of_node, + "ktm,anti-pop", NULL); + const void *ptr_refclk = of_get_property(radio->client->dev.of_node, + "ktm,refclk", NULL); + + if (ptr_anti_pop) { + radio->audio_anti_pop = + clamp(be32_to_cpup(ptr_anti_pop), 0U, 3U); + } else { + radio->audio_anti_pop = 0; + v4l2_warn(radio->client, + "No ktm,anti-pop on dt node, using default"); + } + + if (ptr_refclk) { + radio->refclock_val = + clamp(be32_to_cpup(ptr_refclk), 0U, 9U); + } else { + radio->refclock_val = 0; + v4l2_warn(radio->client, + "No ktm,refclk on dt node, using default"); + } +} + +/* ************************************************************************* */ + +static int kt0913_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct kt0913_device *radio; + struct v4l2_device *v4l2_dev; + struct v4l2_ctrl_handler *hdl; + struct regmap *regmap; + int ret; + + pr_debug("%s\n", __func__); + + /* this driver uses word R/W i2c operations, check if it's supported */ + ret = i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_READ_WORD_DATA | + I2C_FUNC_SMBUS_WRITE_WORD_DATA); + if (!ret) { + v4l2_err(client, + "I2C adapter doesn't support word operations"); + return -EIO; + } + + /* check if the device exist on the bus before initializing it */ + ret = i2c_smbus_read_word_data(client, KT0913_REG_CHIP_ID); + if (ret < 0) { + v4l2_err(client, + "Error reading CHIP ID of the kt0913 (%d)", ret); + return ret; + } + + /* check if the CHIP ID register value matches the expected value */ + if (ret != KT0913_CHIP_ID) { + v4l2_err(radio->client, + "Invalid CHIP ID: 0x%x, expected 0x%x", + ret, KT0913_CHIP_ID); + return -ENODEV; + } + + v4l2_info(client, + "kt0913 found @ 0x%x (%s)\n", + client->addr, client->adapter->name); + + /* alloc context for the kt0913 radio struct */ + radio = devm_kzalloc(&client->dev, sizeof(*radio), GFP_KERNEL); + if (!radio) + return -ENOMEM; + + v4l2_dev = &radio->v4l2_dev; + ret = v4l2_device_register(&client->dev, v4l2_dev); + if (ret < 0) { + v4l2_err(client, + "could not register v4l2_dev\n"); + goto errfr; + } + + mutex_init(&radio->mutex); + + /* register the control handler from the context struct */ + hdl = &radio->ctrl_handler; + v4l2_ctrl_handler_init(hdl, 5); + + /* add the control: Mute */ + radio->ctrl_mute = v4l2_ctrl_new_std(hdl, &kt0913_ctrl_ops, + V4L2_CID_AUDIO_MUTE, + 0, 1, 1, 0); + if (hdl->error) { + ret = hdl->error; + v4l2_err(v4l2_dev, "Could not register control: mute\n"); + goto errunreg; + } + + /* add the control: Volume */ + radio->ctrl_volume = v4l2_ctrl_new_std(hdl, &kt0913_ctrl_ops, + V4L2_CID_AUDIO_VOLUME, + -60, 0, 2, 0); + if (hdl->error) { + ret = hdl->error; + v4l2_err(v4l2_dev, "Could not register control: Volume\n"); + goto errunreg; + } + + /* add the control: audio gain */ + radio->ctrl_au_gain = v4l2_ctrl_new_std(hdl, &kt0913_ctrl_ops, + V4L2_CID_GAIN, + -3, 6, 3, 3); + if (hdl->error) { + ret = hdl->error; + v4l2_err(v4l2_dev, "Could not register control: audio gain\n"); + goto errunreg; + } + radio->ctrl_au_gain->flags |= V4L2_CTRL_FLAG_SLIDER; + + /* add the control: PLL Lock */ + radio->ctrl_pll_lock = v4l2_ctrl_new_std(hdl, &kt0913_ctrl_ops, + V4L2_CID_RF_TUNER_PLL_LOCK, + 0, 1, 1, 0); + if (hdl->error) { + ret = hdl->error; + v4l2_err(v4l2_dev, "Could not register control: pll lock\n"); + goto errunreg; + } + radio->ctrl_pll_lock->flags |= (V4L2_CTRL_FLAG_VOLATILE | + V4L2_CTRL_FLAG_READ_ONLY); + + /* add the control: deemphasis */ + radio->ctrl_deemp = v4l2_ctrl_new_std_menu(hdl, &kt0913_ctrl_ops, + V4L2_CID_TUNE_DEEMPHASIS, + V4L2_DEEMPHASIS_75_uS, + 0, + V4L2_DEEMPHASIS_75_uS); + if (hdl->error) { + ret = hdl->error; + v4l2_err(v4l2_dev, "Could not register control: deemphasis\n"); + goto errunreg; + } + /* the control handler is ready to be used */ + v4l2_dev->ctrl_handler = hdl; + + radio->vdev = kt0913_radio_template; + radio->vdev.lock = &radio->mutex; + radio->vdev.v4l2_dev = v4l2_dev; + video_set_drvdata(&radio->vdev, radio); + + radio->client = client; + i2c_set_clientdata(client, radio); + + /* init the regmap of the kt0913 */ + regmap = devm_regmap_init_i2c(client, &kt0913_regmap_config); + if (IS_ERR(regmap)) { + ret = PTR_ERR(regmap); + v4l2_err(client, + "devm_regmap_init_i2c() failed! %d", ret); + goto errunreg; + } + radio->regmap = regmap; + + __kt0913_parse_dt(radio); + + /* init the kt0913 into a known state */ + ret = __kt0913_init(radio); + if (ret) { + v4l2_err(client, + "__kt0913_init() failed! %d", ret); + goto errunreg; + } + + pm_runtime_get_noresume(&client->dev); + pm_runtime_set_active(&client->dev); + pm_runtime_enable(&client->dev); + pm_runtime_dont_use_autosuspend(&client->dev); + + ret = video_register_device(&radio->vdev, + VFL_TYPE_RADIO, kt0913_v4l2_radio_nr); + if (ret < 0) { + v4l2_err(client, + "Could not register video device!"); + goto error_pm_disable; + } + + v4l2_info(client, "registered."); + return 0; +error_pm_disable: + pm_runtime_disable(&client->dev); + pm_runtime_set_suspended(&client->dev); +errunreg: + v4l2_ctrl_handler_free(hdl); + v4l2_device_unregister(v4l2_dev); +errfr: + __kt0913_set_standby(radio, true); + kfree(radio); + return ret; +} + +static int kt0913_remove(struct i2c_client *client) +{ + struct kt0913_device *radio = i2c_get_clientdata(client); + + pr_debug("%s\n", __func__); + if (!radio) + return -EINVAL; + + __kt0913_set_standby(radio, true); + + pm_runtime_get_sync(&client->dev); + pm_runtime_disable(&client->dev); + pm_runtime_set_suspended(&client->dev); + pm_runtime_put_noidle(&client->dev); + + video_unregister_device(&radio->vdev); + v4l2_ctrl_handler_free(&radio->ctrl_handler); + v4l2_device_unregister(&radio->v4l2_dev); + + v4l2_info(client, "removed."); + return 0; +} + +/* ************************************************************************* */ + +#ifdef CONFIG_PM +static int kt0913_i2c_pm_runtime_suspend(struct device *dev) +{ + struct kt0913_device *radio = i2c_get_clientdata(to_i2c_client(dev)); + + pr_debug("%s\n", __func__); + if (!radio) + return 0; + + return __kt0913_set_standby(radio, true); +} + +static int kt0913_i2c_pm_runtime_resume(struct device *dev) +{ + struct kt0913_device *radio = i2c_get_clientdata(to_i2c_client(dev)); + + pr_debug("%s\n", __func__); + if (!radio) + return 0; + + return __kt0913_set_standby(radio, false); +} +#endif /* CONFIG_PM */ + +static const struct dev_pm_ops kt0913_i2c_pm_ops = { + SET_RUNTIME_PM_OPS(kt0913_i2c_pm_runtime_suspend, + kt0913_i2c_pm_runtime_resume, NULL) +}; + +static const struct i2c_device_id kt0913_idtable[] = { + { "kt0913", 0 }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(i2c, kt0913_idtable); + +static struct i2c_driver kt0913_driver = { + .driver = { + .name = "kt0913", + .of_match_table = of_match_ptr(kt0913_of_match), + .pm = &kt0913_i2c_pm_ops, + }, + .probe = kt0913_probe, + .remove = kt0913_remove, + .id_table = kt0913_idtable, +}; +module_i2c_driver(kt0913_driver); + +MODULE_AUTHOR("Santiago Hormazabal <santiagohssl@gmail.com>"); +MODULE_DESCRIPTION("KTMicro KT0913 AM/FM receiver"); +MODULE_LICENSE("GPL"); +MODULE_VERSION("0.0.2"); + +module_param(kt0913_use_campus_band, int, 0); +MODULE_PARM_DESC(kt0913_use_campus_band, "Use the Campus Band feature (FM range 32MHz-110MHz)"); +module_param(kt0913_v4l2_radio_nr, int, 0); +MODULE_PARM_DESC(kt0913_v4l2_radio_nr, "v4l2 device number to use (i.e. /dev/radioX)");
This chip requires almost no support components and can used over I2C. The driver uses the I2C bus and exposes the controls as a V4L2 radio. Tested with a module that contains this chip (from SZZSJDZ.com, part number ZJ-801B, even tho the company seems defunct now), and an H2+ AllWinner SoC running a kernel built off 8f2a4a9 of the media_tree. Signed-off-by: Santiago Hormazabal <santiagohssl@gmail.com> --- drivers/media/radio/Kconfig | 10 + drivers/media/radio/Makefile | 1 + drivers/media/radio/radio-kt0913.c | 1196 ++++++++++++++++++++++++++++ 3 files changed, 1207 insertions(+) create mode 100644 drivers/media/radio/radio-kt0913.c