linux/drivers/mfd/si476x-i2c.c

879 lines
22 KiB
C
Raw Normal View History

// SPDX-License-Identifier: GPL-2.0-only
/*
* drivers/mfd/si476x-i2c.c -- Core device driver for si476x MFD
* device
*
* 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/slab.h>
#include <linux/interrupt.h>
#include <linux/delay.h>
#include <linux/gpio.h>
#include <linux/regulator/consumer.h>
#include <linux/i2c.h>
#include <linux/err.h>
#include <linux/mfd/si476x-core.h>
#define SI476X_MAX_IO_ERRORS 10
#define SI476X_DRIVER_RDS_FIFO_DEPTH 128
/**
* si476x_core_config_pinmux() - pin function configuration function
*
* @core: Core device structure
*
* Configure the functions of the pins of the radio chip.
*
* The function returns zero in case of succes or negative error code
* otherwise.
*/
static int si476x_core_config_pinmux(struct si476x_core *core)
{
int err;
dev_dbg(&core->client->dev, "Configuring pinmux\n");
err = si476x_core_cmd_dig_audio_pin_cfg(core,
core->pinmux.dclk,
core->pinmux.dfs,
core->pinmux.dout,
core->pinmux.xout);
if (err < 0) {
dev_err(&core->client->dev,
"Failed to configure digital audio pins(err = %d)\n",
err);
return err;
}
err = si476x_core_cmd_zif_pin_cfg(core,
core->pinmux.iqclk,
core->pinmux.iqfs,
core->pinmux.iout,
core->pinmux.qout);
if (err < 0) {
dev_err(&core->client->dev,
"Failed to configure ZIF pins(err = %d)\n",
err);
return err;
}
err = si476x_core_cmd_ic_link_gpo_ctl_pin_cfg(core,
core->pinmux.icin,
core->pinmux.icip,
core->pinmux.icon,
core->pinmux.icop);
if (err < 0) {
dev_err(&core->client->dev,
"Failed to configure IC-Link/GPO pins(err = %d)\n",
err);
return err;
}
err = si476x_core_cmd_ana_audio_pin_cfg(core,
core->pinmux.lrout);
if (err < 0) {
dev_err(&core->client->dev,
"Failed to configure analog audio pins(err = %d)\n",
err);
return err;
}
err = si476x_core_cmd_intb_pin_cfg(core,
core->pinmux.intb,
core->pinmux.a1);
if (err < 0) {
dev_err(&core->client->dev,
"Failed to configure interrupt pins(err = %d)\n",
err);
return err;
}
return 0;
}
static inline void si476x_core_schedule_polling_work(struct si476x_core *core)
{
schedule_delayed_work(&core->status_monitor,
usecs_to_jiffies(SI476X_STATUS_POLL_US));
}
/**
* si476x_core_start() - early chip startup function
* @core: Core device structure
* @soft: When set, this flag forces "soft" startup, where "soft"
* power down is the one done by sending appropriate command instead
* of using reset pin of the tuner
*
* Perform required startup sequence to correctly power
* up the chip and perform initial configuration. It does the
* following sequence of actions:
* 1. Claims and enables the power supplies VD and VIO1 required
* for I2C interface of the chip operation.
* 2. Waits for 100us, pulls the reset line up, enables irq,
* waits for another 100us as it is specified by the
* datasheet.
* 3. Sends 'POWER_UP' command to the device with all provided
* information about power-up parameters.
* 4. Configures, pin multiplexor, disables digital audio and
* configures interrupt sources.
*
* The function returns zero in case of succes or negative error code
* otherwise.
*/
int si476x_core_start(struct si476x_core *core, bool soft)
{
struct i2c_client *client = core->client;
int err;
if (!soft) {
if (gpio_is_valid(core->gpio_reset))
gpio_set_value_cansleep(core->gpio_reset, 1);
if (client->irq)
enable_irq(client->irq);
udelay(100);
if (!client->irq) {
atomic_set(&core->is_alive, 1);
si476x_core_schedule_polling_work(core);
}
} else {
if (client->irq)
enable_irq(client->irq);
else {
atomic_set(&core->is_alive, 1);
si476x_core_schedule_polling_work(core);
}
}
err = si476x_core_cmd_power_up(core,
&core->power_up_parameters);
if (err < 0) {
dev_err(&core->client->dev,
"Power up failure(err = %d)\n",
err);
goto disable_irq;
}
if (client->irq)
atomic_set(&core->is_alive, 1);
err = si476x_core_config_pinmux(core);
if (err < 0) {
dev_err(&core->client->dev,
"Failed to configure pinmux(err = %d)\n",
err);
goto disable_irq;
}
if (client->irq) {
err = regmap_write(core->regmap,
SI476X_PROP_INT_CTL_ENABLE,
SI476X_RDSIEN |
SI476X_STCIEN |
SI476X_CTSIEN);
if (err < 0) {
dev_err(&core->client->dev,
"Failed to configure interrupt sources"
"(err = %d)\n", err);
goto disable_irq;
}
}
return 0;
disable_irq:
if (err == -ENODEV)
atomic_set(&core->is_alive, 0);
if (client->irq)
disable_irq(client->irq);
else
cancel_delayed_work_sync(&core->status_monitor);
if (gpio_is_valid(core->gpio_reset))
gpio_set_value_cansleep(core->gpio_reset, 0);
return err;
}
EXPORT_SYMBOL_GPL(si476x_core_start);
/**
* si476x_core_stop() - chip power-down function
* @core: Core device structure
* @soft: When set, function sends a POWER_DOWN command instead of
* bringing reset line low
*
* Power down the chip by performing following actions:
* 1. Disable IRQ or stop the polling worker
* 2. Send the POWER_DOWN command if the power down is soft or bring
* reset line low if not.
*
* The function returns zero in case of succes or negative error code
* otherwise.
*/
int si476x_core_stop(struct si476x_core *core, bool soft)
{
int err = 0;
atomic_set(&core->is_alive, 0);
if (soft) {
/* TODO: This probably shoud be a configurable option,
* so it is possible to have the chips keep their
* oscillators running
*/
struct si476x_power_down_args args = {
.xosc = false,
};
err = si476x_core_cmd_power_down(core, &args);
}
/* We couldn't disable those before
* 'si476x_core_cmd_power_down' since we expect to get CTS
* interrupt */
if (core->client->irq)
disable_irq(core->client->irq);
else
cancel_delayed_work_sync(&core->status_monitor);
if (!soft) {
if (gpio_is_valid(core->gpio_reset))
gpio_set_value_cansleep(core->gpio_reset, 0);
}
return err;
}
EXPORT_SYMBOL_GPL(si476x_core_stop);
/**
* si476x_core_set_power_state() - set the level at which the power is
* supplied for the chip.
* @core: Core device structure
* @next_state: enum si476x_power_state describing power state to
* switch to.
*
* Switch on all the required power supplies
*
* This function returns 0 in case of suvccess and negative error code
* otherwise.
*/
int si476x_core_set_power_state(struct si476x_core *core,
enum si476x_power_state next_state)
{
/*
It is not clear form the datasheet if it is possible to
work with device if not all power domains are operational.
So for now the power-up policy is "power-up all the things!"
*/
int err = 0;
if (core->power_state == SI476X_POWER_INCONSISTENT) {
dev_err(&core->client->dev,
"The device in inconsistent power state\n");
return -EINVAL;
}
if (next_state != core->power_state) {
switch (next_state) {
case SI476X_POWER_UP_FULL:
err = regulator_bulk_enable(ARRAY_SIZE(core->supplies),
core->supplies);
if (err < 0) {
core->power_state = SI476X_POWER_INCONSISTENT;
break;
}
/*
* Startup timing diagram recommends to have a
* 100 us delay between enabling of the power
* supplies and turning the tuner on.
*/
udelay(100);
err = si476x_core_start(core, false);
if (err < 0)
goto disable_regulators;
core->power_state = next_state;
break;
case SI476X_POWER_DOWN:
core->power_state = next_state;
err = si476x_core_stop(core, false);
if (err < 0)
core->power_state = SI476X_POWER_INCONSISTENT;
disable_regulators:
err = regulator_bulk_disable(ARRAY_SIZE(core->supplies),
core->supplies);
if (err < 0)
core->power_state = SI476X_POWER_INCONSISTENT;
break;
default:
BUG();
}
}
return err;
}
EXPORT_SYMBOL_GPL(si476x_core_set_power_state);
/**
* si476x_core_report_drainer_stop() - mark the completion of the RDS
* buffer drain porcess by the worker.
*
* @core: Core device structure
*/
static inline void si476x_core_report_drainer_stop(struct si476x_core *core)
{
mutex_lock(&core->rds_drainer_status_lock);
core->rds_drainer_is_working = false;
mutex_unlock(&core->rds_drainer_status_lock);
}
/**
* si476x_core_start_rds_drainer_once() - start RDS drainer worker if
* ther is none working, do nothing otherwise
*
* @core: Datastructure corresponding to the chip.
*/
static inline void si476x_core_start_rds_drainer_once(struct si476x_core *core)
{
mutex_lock(&core->rds_drainer_status_lock);
if (!core->rds_drainer_is_working) {
core->rds_drainer_is_working = true;
schedule_work(&core->rds_fifo_drainer);
}
mutex_unlock(&core->rds_drainer_status_lock);
}
/**
* si476x_core_drain_rds_fifo() - RDS buffer drainer.
* @work: struct work_struct being ppassed to the function by the
* kernel.
*
* Drain the contents of the RDS FIFO of
*/
static void si476x_core_drain_rds_fifo(struct work_struct *work)
{
int err;
struct si476x_core *core = container_of(work, struct si476x_core,
rds_fifo_drainer);
struct si476x_rds_status_report report;
si476x_core_lock(core);
err = si476x_core_cmd_fm_rds_status(core, true, false, false, &report);
if (!err) {
int i = report.rdsfifoused;
dev_dbg(&core->client->dev,
"%d elements in RDS FIFO. Draining.\n", i);
for (; i > 0; --i) {
err = si476x_core_cmd_fm_rds_status(core, false, false,
(i == 1), &report);
if (err < 0)
goto unlock;
kfifo_in(&core->rds_fifo, report.rds,
sizeof(report.rds));
dev_dbg(&core->client->dev, "RDS data:\n %*ph\n",
(int)sizeof(report.rds), report.rds);
}
dev_dbg(&core->client->dev, "Drrrrained!\n");
wake_up_interruptible(&core->rds_read_queue);
}
unlock:
si476x_core_unlock(core);
si476x_core_report_drainer_stop(core);
}
/**
* si476x_core_pronounce_dead()
*
* @core: Core device structure
*
* Mark the device as being dead and wake up all potentially waiting
* threads of execution.
*
*/
static void si476x_core_pronounce_dead(struct si476x_core *core)
{
dev_info(&core->client->dev, "Core device is dead.\n");
atomic_set(&core->is_alive, 0);
/* Wake up al possible waiting processes */
wake_up_interruptible(&core->rds_read_queue);
atomic_set(&core->cts, 1);
wake_up(&core->command);
atomic_set(&core->stc, 1);
wake_up(&core->tuning);
}
/**
* si476x_core_i2c_xfer()
*
* @core: Core device structure
* @type: Transfer type
* @buf: Transfer buffer for/with data
* @count: Transfer buffer size
*
* Perfrom and I2C transfer(either read or write) and keep a counter
* of I/O errors. If the error counter rises above the threshold
* pronounce device dead.
*
* The function returns zero on succes or negative error code on
* failure.
*/
int si476x_core_i2c_xfer(struct si476x_core *core,
enum si476x_i2c_type type,
char *buf, int count)
{
static int io_errors_count;
int err;
if (type == SI476X_I2C_SEND)
err = i2c_master_send(core->client, buf, count);
else
err = i2c_master_recv(core->client, buf, count);
if (err < 0) {
if (io_errors_count++ > SI476X_MAX_IO_ERRORS)
si476x_core_pronounce_dead(core);
} else {
io_errors_count = 0;
}
return err;
}
EXPORT_SYMBOL_GPL(si476x_core_i2c_xfer);
/**
* si476x_core_get_status()
* @core: Core device structure
*
* Get the status byte of the core device by berforming one byte I2C
* read.
*
* The function returns a status value or a negative error code on
* error.
*/
static int si476x_core_get_status(struct si476x_core *core)
{
u8 response;
int err = si476x_core_i2c_xfer(core, SI476X_I2C_RECV,
&response, sizeof(response));
return (err < 0) ? err : response;
}
/**
* si476x_core_get_and_signal_status() - IRQ dispatcher
* @core: Core device structure
*
* Dispatch the arrived interrupt request based on the value of the
* status byte reported by the tuner.
*
*/
static void si476x_core_get_and_signal_status(struct si476x_core *core)
{
int status = si476x_core_get_status(core);
if (status < 0) {
dev_err(&core->client->dev, "Failed to get status\n");
return;
}
if (status & SI476X_CTS) {
/* Unfortunately completions could not be used for
* signalling CTS since this flag cannot be cleared
* in status byte, and therefore once it becomes true
* multiple calls to 'complete' would cause the
* commands following the current one to be completed
* before they actually are */
dev_dbg(&core->client->dev, "[interrupt] CTSINT\n");
atomic_set(&core->cts, 1);
wake_up(&core->command);
}
if (status & SI476X_FM_RDS_INT) {
dev_dbg(&core->client->dev, "[interrupt] RDSINT\n");
si476x_core_start_rds_drainer_once(core);
}
if (status & SI476X_STC_INT) {
dev_dbg(&core->client->dev, "[interrupt] STCINT\n");
atomic_set(&core->stc, 1);
wake_up(&core->tuning);
}
}
static void si476x_core_poll_loop(struct work_struct *work)
{
struct si476x_core *core = SI476X_WORK_TO_CORE(work);
si476x_core_get_and_signal_status(core);
if (atomic_read(&core->is_alive))
si476x_core_schedule_polling_work(core);
}
static irqreturn_t si476x_core_interrupt(int irq, void *dev)
{
struct si476x_core *core = dev;
si476x_core_get_and_signal_status(core);
return IRQ_HANDLED;
}
/**
* si476x_core_fwver_to_revision()
* @core: Core device structure
* @func: Selects the boot function of the device:
* *_BOOTLOADER - Boot loader
* *_FM_RECEIVER - FM receiver
* *_AM_RECEIVER - AM receiver
* *_WB_RECEIVER - Weatherband receiver
* @major: Firmware major number
* @minor1: Firmware first minor number
* @minor2: Firmware second minor number
*
* Convert a chip's firmware version number into an offset that later
* will be used to as offset in "vtable" of tuner functions
*
* This function returns a positive offset in case of success and a -1
* in case of failure.
*/
static int si476x_core_fwver_to_revision(struct si476x_core *core,
int func, int major,
int minor1, int minor2)
{
switch (func) {
case SI476X_FUNC_FM_RECEIVER:
switch (major) {
case 5:
return SI476X_REVISION_A10;
case 8:
return SI476X_REVISION_A20;
case 10:
return SI476X_REVISION_A30;
default:
goto unknown_revision;
}
case SI476X_FUNC_AM_RECEIVER:
switch (major) {
case 5:
return SI476X_REVISION_A10;
case 7:
return SI476X_REVISION_A20;
case 9:
return SI476X_REVISION_A30;
default:
goto unknown_revision;
}
case SI476X_FUNC_WB_RECEIVER:
switch (major) {
case 3:
return SI476X_REVISION_A10;
case 5:
return SI476X_REVISION_A20;
case 7:
return SI476X_REVISION_A30;
default:
goto unknown_revision;
}
case SI476X_FUNC_BOOTLOADER:
default: /* FALLTHROUGH */
BUG();
return -1;
}
unknown_revision:
dev_err(&core->client->dev,
"Unsupported version of the firmware: %d.%d.%d, "
"reverting to A10 compatible functions\n",
major, minor1, minor2);
return SI476X_REVISION_A10;
}
/**
* si476x_core_get_revision_info()
* @core: Core device structure
*
* Get the firmware version number of the device. It is done in
* following three steps:
* 1. Power-up the device
* 2. Send the 'FUNC_INFO' command
* 3. Powering the device down.
*
* The function return zero on success and a negative error code on
* failure.
*/
static int si476x_core_get_revision_info(struct si476x_core *core)
{
int rval;
struct si476x_func_info info;
si476x_core_lock(core);
rval = si476x_core_set_power_state(core, SI476X_POWER_UP_FULL);
if (rval < 0)
goto exit;
rval = si476x_core_cmd_func_info(core, &info);
if (rval < 0)
goto power_down;
core->revision = si476x_core_fwver_to_revision(core, info.func,
info.firmware.major,
info.firmware.minor[0],
info.firmware.minor[1]);
power_down:
si476x_core_set_power_state(core, SI476X_POWER_DOWN);
exit:
si476x_core_unlock(core);
return rval;
}
bool si476x_core_has_am(struct si476x_core *core)
{
return core->chip_id == SI476X_CHIP_SI4761 ||
core->chip_id == SI476X_CHIP_SI4764;
}
EXPORT_SYMBOL_GPL(si476x_core_has_am);
bool si476x_core_has_diversity(struct si476x_core *core)
{
return core->chip_id == SI476X_CHIP_SI4764;
}
EXPORT_SYMBOL_GPL(si476x_core_has_diversity);
bool si476x_core_is_a_secondary_tuner(struct si476x_core *core)
{
return si476x_core_has_diversity(core) &&
(core->diversity_mode == SI476X_PHDIV_SECONDARY_ANTENNA ||
core->diversity_mode == SI476X_PHDIV_SECONDARY_COMBINING);
}
EXPORT_SYMBOL_GPL(si476x_core_is_a_secondary_tuner);
bool si476x_core_is_a_primary_tuner(struct si476x_core *core)
{
return si476x_core_has_diversity(core) &&
(core->diversity_mode == SI476X_PHDIV_PRIMARY_ANTENNA ||
core->diversity_mode == SI476X_PHDIV_PRIMARY_COMBINING);
}
EXPORT_SYMBOL_GPL(si476x_core_is_a_primary_tuner);
bool si476x_core_is_in_am_receiver_mode(struct si476x_core *core)
{
return si476x_core_has_am(core) &&
(core->power_up_parameters.func == SI476X_FUNC_AM_RECEIVER);
}
EXPORT_SYMBOL_GPL(si476x_core_is_in_am_receiver_mode);
bool si476x_core_is_powered_up(struct si476x_core *core)
{
return core->power_state == SI476X_POWER_UP_FULL;
}
EXPORT_SYMBOL_GPL(si476x_core_is_powered_up);
static int si476x_core_probe(struct i2c_client *client)
{
const struct i2c_device_id *id = i2c_client_get_device_id(client);
int rval;
struct si476x_core *core;
struct si476x_platform_data *pdata;
struct mfd_cell *cell;
int cell_num;
core = devm_kzalloc(&client->dev, sizeof(*core), GFP_KERNEL);
if (!core)
return -ENOMEM;
core->client = client;
core->regmap = devm_regmap_init_si476x(core);
if (IS_ERR(core->regmap)) {
rval = PTR_ERR(core->regmap);
dev_err(&client->dev,
"Failed to allocate register map: %d\n",
rval);
return rval;
}
i2c_set_clientdata(client, core);
atomic_set(&core->is_alive, 0);
core->power_state = SI476X_POWER_DOWN;
pdata = dev_get_platdata(&client->dev);
if (pdata) {
memcpy(&core->power_up_parameters,
&pdata->power_up_parameters,
sizeof(core->power_up_parameters));
core->gpio_reset = -1;
if (gpio_is_valid(pdata->gpio_reset)) {
rval = gpio_request(pdata->gpio_reset, "si476x reset");
if (rval) {
dev_err(&client->dev,
"Failed to request gpio: %d\n", rval);
return rval;
}
core->gpio_reset = pdata->gpio_reset;
gpio_direction_output(core->gpio_reset, 0);
}
core->diversity_mode = pdata->diversity_mode;
memcpy(&core->pinmux, &pdata->pinmux,
sizeof(struct si476x_pinmux));
} else {
dev_err(&client->dev, "No platform data provided\n");
return -EINVAL;
}
core->supplies[0].supply = "vd";
core->supplies[1].supply = "va";
core->supplies[2].supply = "vio1";
core->supplies[3].supply = "vio2";
rval = devm_regulator_bulk_get(&client->dev,
ARRAY_SIZE(core->supplies),
core->supplies);
if (rval) {
dev_err(&client->dev, "Failed to get all of the regulators\n");
goto free_gpio;
}
mutex_init(&core->cmd_lock);
init_waitqueue_head(&core->command);
init_waitqueue_head(&core->tuning);
rval = kfifo_alloc(&core->rds_fifo,
SI476X_DRIVER_RDS_FIFO_DEPTH *
sizeof(struct v4l2_rds_data),
GFP_KERNEL);
if (rval) {
dev_err(&client->dev, "Could not allocate the FIFO\n");
goto free_gpio;
}
mutex_init(&core->rds_drainer_status_lock);
init_waitqueue_head(&core->rds_read_queue);
INIT_WORK(&core->rds_fifo_drainer, si476x_core_drain_rds_fifo);
if (client->irq) {
rval = devm_request_threaded_irq(&client->dev,
client->irq, NULL,
si476x_core_interrupt,
IRQF_TRIGGER_FALLING |
IRQF_ONESHOT,
client->name, core);
if (rval < 0) {
dev_err(&client->dev, "Could not request IRQ %d\n",
client->irq);
goto free_kfifo;
}
disable_irq(client->irq);
dev_dbg(&client->dev, "IRQ requested.\n");
core->rds_fifo_depth = 20;
} else {
INIT_DELAYED_WORK(&core->status_monitor,
si476x_core_poll_loop);
dev_info(&client->dev,
"No IRQ number specified, will use polling\n");
core->rds_fifo_depth = 5;
}
core->chip_id = id->driver_data;
rval = si476x_core_get_revision_info(core);
if (rval < 0) {
rval = -ENODEV;
goto free_kfifo;
}
cell_num = 0;
cell = &core->cells[SI476X_RADIO_CELL];
cell->name = "si476x-radio";
cell_num++;
#ifdef CONFIG_SND_SOC_SI476X
if ((core->chip_id == SI476X_CHIP_SI4761 ||
core->chip_id == SI476X_CHIP_SI4764) &&
core->pinmux.dclk == SI476X_DCLK_DAUDIO &&
core->pinmux.dfs == SI476X_DFS_DAUDIO &&
core->pinmux.dout == SI476X_DOUT_I2S_OUTPUT &&
core->pinmux.xout == SI476X_XOUT_TRISTATE) {
cell = &core->cells[SI476X_CODEC_CELL];
cell->name = "si476x-codec";
cell_num++;
}
#endif
rval = mfd_add_devices(&client->dev,
(client->adapter->nr << 8) + client->addr,
core->cells, cell_num,
NULL, 0, NULL);
if (!rval)
return 0;
free_kfifo:
kfifo_free(&core->rds_fifo);
free_gpio:
if (gpio_is_valid(core->gpio_reset))
gpio_free(core->gpio_reset);
return rval;
}
i2c: Make remove callback return void The value returned by an i2c driver's remove function is mostly ignored. (Only an error message is printed if the value is non-zero that the error is ignored.) So change the prototype of the remove function to return no value. This way driver authors are not tempted to assume that passing an error to the upper layer is a good idea. All drivers are adapted accordingly. There is no intended change of behaviour, all callbacks were prepared to return 0 before. Reviewed-by: Peter Senna Tschudin <peter.senna@gmail.com> Reviewed-by: Jeremy Kerr <jk@codeconstruct.com.au> Reviewed-by: Benjamin Mugnier <benjamin.mugnier@foss.st.com> Reviewed-by: Javier Martinez Canillas <javierm@redhat.com> Reviewed-by: Crt Mori <cmo@melexis.com> Reviewed-by: Heikki Krogerus <heikki.krogerus@linux.intel.com> Acked-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org> Acked-by: Marek Behún <kabel@kernel.org> # for leds-turris-omnia Acked-by: Andy Shevchenko <andriy.shevchenko@linux.intel.com> Reviewed-by: Petr Machata <petrm@nvidia.com> # for mlxsw Reviewed-by: Maximilian Luz <luzmaximilian@gmail.com> # for surface3_power Acked-by: Srinivas Pandruvada <srinivas.pandruvada@linux.intel.com> # for bmc150-accel-i2c + kxcjk-1013 Reviewed-by: Hans Verkuil <hverkuil-cisco@xs4all.nl> # for media/* + staging/media/* Acked-by: Miguel Ojeda <ojeda@kernel.org> # for auxdisplay/ht16k33 + auxdisplay/lcd2s Reviewed-by: Luca Ceresoli <luca.ceresoli@bootlin.com> # for versaclock5 Reviewed-by: Ajay Gupta <ajayg@nvidia.com> # for ucsi_ccg Acked-by: Jonathan Cameron <Jonathan.Cameron@huawei.com> # for iio Acked-by: Peter Rosin <peda@axentia.se> # for i2c-mux-*, max9860 Acked-by: Adrien Grassein <adrien.grassein@gmail.com> # for lontium-lt8912b Reviewed-by: Jean Delvare <jdelvare@suse.de> # for hwmon, i2c-core and i2c/muxes Acked-by: Corey Minyard <cminyard@mvista.com> # for IPMI Reviewed-by: Vladimir Oltean <olteanv@gmail.com> Acked-by: Dmitry Torokhov <dmitry.torokhov@gmail.com> Acked-by: Sebastian Reichel <sebastian.reichel@collabora.com> # for drivers/power Acked-by: Krzysztof Hałasa <khalasa@piap.pl> Signed-off-by: Uwe Kleine-König <u.kleine-koenig@pengutronix.de> Signed-off-by: Wolfram Sang <wsa@kernel.org>
2022-08-15 16:02:30 +08:00
static void si476x_core_remove(struct i2c_client *client)
{
struct si476x_core *core = i2c_get_clientdata(client);
si476x_core_pronounce_dead(core);
mfd_remove_devices(&client->dev);
if (client->irq)
disable_irq(client->irq);
else
cancel_delayed_work_sync(&core->status_monitor);
kfifo_free(&core->rds_fifo);
if (gpio_is_valid(core->gpio_reset))
gpio_free(core->gpio_reset);
}
static const struct i2c_device_id si476x_id[] = {
{ "si4761", SI476X_CHIP_SI4761 },
{ "si4764", SI476X_CHIP_SI4764 },
{ "si4768", SI476X_CHIP_SI4768 },
{ },
};
MODULE_DEVICE_TABLE(i2c, si476x_id);
static struct i2c_driver si476x_core_driver = {
.driver = {
.name = "si476x-core",
},
.probe_new = si476x_core_probe,
.remove = si476x_core_remove,
.id_table = si476x_id,
};
module_i2c_driver(si476x_core_driver);
MODULE_AUTHOR("Andrey Smirnov <andrew.smirnov@gmail.com>");
MODULE_DESCRIPTION("Si4761/64/68 AM/FM MFD core device driver");
MODULE_LICENSE("GPL");