// SPDX-License-Identifier: GPL-2.0-only
/*
* drivers/media/radio/radio-si476x.c -- V4L2 driver for SI476X chips
*
* Copyright (C) 2012 Innovative Converged Devices(ICD)
* Copyright (C) 2013 Andrey Smirnov
*
* Author: Andrey Smirnov <andrew.smirnov@gmail.com>
*/
#include <linux/module.h>
#include <linux/delay.h>
#include <linux/interrupt.h>
#include <linux/slab.h>
#include <linux/atomic.h>
#include <linux/videodev2.h>
#include <linux/mutex.h>
#include <linux/debugfs.h>
#include <media/v4l2-common.h>
#include <media/v4l2-ioctl.h>
#include <media/v4l2-ctrls.h>
#include <media/v4l2-event.h>
#include <media/v4l2-device.h>
#include <media/drv-intf/si476x.h>
#include <linux/mfd/si476x-core.h>
#define FM_FREQ_RANGE_LOW 64000000
#define FM_FREQ_RANGE_HIGH 108000000
#define AM_FREQ_RANGE_LOW 520000
#define AM_FREQ_RANGE_HIGH 30000000
#define PWRLINEFLTR (1 << 8)
#define FREQ_MUL (10000000 / 625)
#define SI476X_PHDIV_STATUS_LINK_LOCKED(status) (0x80 & (status))
#define DRIVER_NAME "si476x-radio"
#define DRIVER_CARD "SI476x AM/FM Receiver"
enum si476x_freq_bands {
SI476X_BAND_FM,
SI476X_BAND_AM,
};
static const struct v4l2_frequency_band si476x_bands[] = {
[SI476X_BAND_FM] = {
.type = V4L2_TUNER_RADIO,
.index = SI476X_BAND_FM,
.capability = V4L2_TUNER_CAP_LOW
| V4L2_TUNER_CAP_STEREO
| V4L2_TUNER_CAP_RDS
| V4L2_TUNER_CAP_RDS_BLOCK_IO
| V4L2_TUNER_CAP_FREQ_BANDS,
.rangelow = 64 * FREQ_MUL,
.rangehigh = 108 * FREQ_MUL,
.modulation = V4L2_BAND_MODULATION_FM,
},
[SI476X_BAND_AM] = {
.type = V4L2_TUNER_RADIO,
.index = SI476X_BAND_AM,
.capability = V4L2_TUNER_CAP_LOW
| V4L2_TUNER_CAP_FREQ_BANDS,
.rangelow = 0.52 * FREQ_MUL,
.rangehigh = 30 * FREQ_MUL,
.modulation = V4L2_BAND_MODULATION_AM,
},
};
static inline bool si476x_radio_freq_is_inside_of_the_band(u32 freq, int band)
{
return freq >= si476x_bands[band].rangelow &&
freq <= si476x_bands[band].rangehigh;
}
static inline bool si476x_radio_range_is_inside_of_the_band(u32 low, u32 high,
int band)
{
return low >= si476x_bands[band].rangelow &&
high <= si476x_bands[band].rangehigh;
}
static int si476x_radio_s_ctrl(struct v4l2_ctrl *ctrl);
static int si476x_radio_g_volatile_ctrl(struct v4l2_ctrl *ctrl);
enum phase_diversity_modes_idx {
SI476X_IDX_PHDIV_DISABLED,
SI476X_IDX_PHDIV_PRIMARY_COMBINING,
SI476X_IDX_PHDIV_PRIMARY_ANTENNA,
SI476X_IDX_PHDIV_SECONDARY_ANTENNA,
SI476X_IDX_PHDIV_SECONDARY_COMBINING,
};
static const char * const phase_diversity_modes[] = {
[SI476X_IDX_PHDIV_DISABLED] = "Disabled",
[SI476X_IDX_PHDIV_PRIMARY_COMBINING] = "Primary with Secondary",
[SI476X_IDX_PHDIV_PRIMARY_ANTENNA] = "Primary Antenna",
[SI476X_IDX_PHDIV_SECONDARY_ANTENNA] = "Secondary Antenna",
[SI476X_IDX_PHDIV_SECONDARY_COMBINING] = "Secondary with Primary",
};
static inline enum phase_diversity_modes_idx
si476x_phase_diversity_mode_to_idx(enum si476x_phase_diversity_mode mode)
{
switch (mode) {
default: /* FALLTHROUGH */
case SI476X_PHDIV_DISABLED:
return SI476X_IDX_PHDIV_DISABLED;
case SI476X_PHDIV_PRIMARY_COMBINING:
return SI476X_IDX_PHDIV_PRIMARY_COMBINING;
case SI476X_PHDIV_PRIMARY_ANTENNA:
return SI476X_IDX_PHDIV_PRIMARY_ANTENNA;
case SI476X_PHDIV_SECONDARY_ANTENNA:
return SI476X_IDX_PHDIV_SECONDARY_ANTENNA;
case SI476X_PHDIV_SECONDARY_COMBINING:
return SI476X_IDX_PHDIV_SECONDARY_COMBINING;
}
}
static inline enum si476x_phase_diversity_mode
si476x_phase_diversity_idx_to_mode(enum phase_diversity_modes_idx idx)
{
static const int idx_to_value[] = {
[SI476X_IDX_PHDIV_DISABLED] = SI476X_PHDIV_DISABLED,
[SI476X_IDX_PHDIV_PRIMARY_COMBINING] = SI476X_PHDIV_PRIMARY_COMBINING,
[SI476X_IDX_PHDIV_PRIMARY_ANTENNA] = SI476X_PHDIV_PRIMARY_ANTENNA,
[SI476X_IDX_PHDIV_SECONDARY_ANTENNA] = SI476X_PHDIV_SECONDARY_ANTENNA,
[SI476X_IDX_PHDIV_SECONDARY_COMBINING] = SI476X_PHDIV_SECONDARY_COMBINING,
};
return idx_to_value[idx];
}
static const struct v4l2_ctrl_ops si476x_ctrl_ops = {
.g_volatile_ctrl = si476x_radio_g_volatile_ctrl,
.s_ctrl = si476x_radio_s_ctrl,
};
enum si476x_ctrl_idx {
SI476X_IDX_RSSI_THRESHOLD,
SI476X_IDX_SNR_THRESHOLD,
SI476X_IDX_MAX_TUNE_ERROR,
SI476X_IDX_HARMONICS_COUNT,
SI476X_IDX_DIVERSITY_MODE,
SI476X_IDX_INTERCHIP_LINK,
};
static struct v4l2_ctrl_config si476x_ctrls[] = {
/*
* SI476X during its station seeking(or tuning) process uses several
* parameters to detrmine if "the station" is valid:
*
* - Signal's SNR(in dBuV) must be lower than
* #V4L2_CID_SI476X_SNR_THRESHOLD
* - Signal's RSSI(in dBuV) must be greater than
* #V4L2_CID_SI476X_RSSI_THRESHOLD
* - Signal's frequency deviation(in units of 2ppm) must not be
* more than #V4L2_CID_SI476X_MAX_TUNE_ERROR
*/
[SI476X_IDX_RSSI_THRESHOLD] = {
.ops = &si476x_ctrl_ops,
.id = V4L2_CID_SI476X_RSSI_THRESHOLD,
.name = "Valid RSSI Threshold",
.type = V4L2_CTRL_TYPE_INTEGER,
.min = -128,
.max = 127,
.step = 1,
},
[SI476X_IDX_SNR_THRESHOLD] = {
.ops = &si476x_ctrl_ops,
.id = V4L2_CID_SI476X_SNR_THRESHOLD,
.type = V4L2_CTRL_TYPE_INTEGER,
.name = "Valid SNR Threshold",
.min = -128,
.max = 127,
.step = 1,
},
[SI476X_IDX_MAX_TUNE_ERROR] = {
.ops = &si476x_ctrl_ops,
.id = V4L2_CID_SI476X_MAX_TUNE_ERROR,
.type = V4L2_CTRL_TYPE_INTEGER,
.name = "Max Tune Errors",
.min = 0,
.max = 126 * 2,
.step = 2,
},
/*
* #V4L2_CID_SI476X_HARMONICS_COUNT -- number of harmonics
* built-in power-line noise supression filter is to reject
* during AM-mode operation.
*/
[SI476X_IDX_HARMONICS_COUNT] = {
.ops = &si476x_ctrl_ops,
.id = V4L2_CID_SI476X_HARMONIC
|