diff mbox

[v3,2/6] Add the main bulk of core driver for SI476x code

Message ID 1351017872-32488-3-git-send-email-andrey.smirnov@convergeddevices.net (mailing list archive)
State New, archived
Headers show

Commit Message

Andrey Smirnov Oct. 23, 2012, 6:44 p.m. UTC
This patch adds main part(out of three) of the I2C driver for the
"core" of MFD device.

Signed-off-by: Andrey Smirnov <andrey.smirnov@convergeddevices.net>
---
 drivers/mfd/si476x-i2c.c |  966 ++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 966 insertions(+)
 create mode 100644 drivers/mfd/si476x-i2c.c

Comments

Mark Brown Oct. 25, 2012, 7:45 p.m. UTC | #1
On Tue, Oct 23, 2012 at 11:44:28AM -0700, Andrey Smirnov wrote:

> +	core->regmap = devm_regmap_init_si476x(core);
> +	if (IS_ERR(core->regmap)) {

This really makes little sense to me, why are you doing this?  Does the
device *really* layer a byte stream on top of I2C for sending messages
that look like marshalled register reads and writes?
Andrey Smirnov Oct. 25, 2012, 10:26 p.m. UTC | #2
On 10/25/2012 12:45 PM, Mark Brown wrote:
> On Tue, Oct 23, 2012 at 11:44:28AM -0700, Andrey Smirnov wrote:
>
>> +	core->regmap = devm_regmap_init_si476x(core);
>> +	if (IS_ERR(core->regmap)) {
> This really makes little sense to me, why are you doing this?  Does the
> device *really* layer a byte stream on top of I2C for sending messages
> that look like marshalled register reads and writes?
The SI476x chips has a concept of a "property". Each property having
16-bit address and 16-bit value. At least a portion of a chip
configuration is done by modifying those properties. In order to
manipulate those properties user is expected to issue what is called a
"command". For manipulating "properties" there are two "commands":
- SET_PROPERTY which is I2C write of 6 bytes of the following layout:
    | 0x13 | 0x00 | Address High Byte | Address Low Byte | Property Data
High Byte | Property Data Low Byte |
    After the command is finished being executed the 1 byte read would
contain the status byte

    followed by 1 byte read which contain status byte
- GET_PROPERTY which is I2C write of 4 bytes of the following layout:
    | 0x13 | 0x00 | Address High Byte | Address Low Byte |
    After the command is finished being executed the 4 byte read would
have the following layout:
    | Status Byte | Reserved Byte | Property Value High Byte | Property
Value Low Byte |

The chip does not operate continuously in the AM/FM frequency range,
instead the user is expected to power-down/power-up the chip in a
certain "mode" which in my case can be either AM or FM tuner. There are
two ways of doing that one is to send a power down command to the
device, the second one is to toggle reset line of the chip. Both methods
will reset the values of the aforementioned "properties". Because V4L2
user-space interface presents a tuner as the one continuously operating
in the whole AM/FM range it is necessary for me to do those AM/FM
switches transparently when handling tuning or seeking requests from the
user. That means that I need to cache the values of the properties I
care about in the driver and restore them when user switches to the
mode(for example when AM->FM->AM transition happens)

The other quirk of that chip is that some properties are only accessible
in certain modes(for example it is impossible to configure RDS interrupt
sources, which is FM specific, in AM mode), but some of the controls I
expose to user-land change the values of the properties and since AM/FM
switches happen transparently to the user in the situation when FM
specific property is changed while tuner is in AM mode, the driver has
to cache the value and write it when the switch to FM would take place.

Also due to the way the driver uses the chip it is only powered up when
the corresponding file in devfs(e.g. /dev/radio0) is opened at least by
one user which means that unless there is a user who opened the file all
the SET/GET_PROPERTY commands sent to it will be lost. The codec driver
for that chip does not have any say in the power management policy(while
all the audio configuration is done via "properties") if the chip is not
powered up the driver has to cache the configuration values it has so
that they can be applied later.

So, since I have to implement a caching functionality in the driver, in
order to avoid reinventing the wheel I opted for using 'regmap' API for
this.
Of course, It is possible that I misunderstood the purpose and
capabilities of the 'regmap' framework, which would make my code look
very silly indeed. If that is the case I'll just re-implement it using
some sort of ad-hoc version of caching.



--
To unsubscribe from this list: send the line "unsubscribe linux-media" in
the body of a message to majordomo@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html
Mark Brown Oct. 27, 2012, 9:31 p.m. UTC | #3
On Thu, Oct 25, 2012 at 03:26:02PM -0700, Andrey Smirnov wrote:
> On 10/25/2012 12:45 PM, Mark Brown wrote:

> > This really makes little sense to me, why are you doing this?  Does the
> > device *really* layer a byte stream on top of I2C for sending messages
> > that look like marshalled register reads and writes?

> The SI476x chips has a concept of a "property". Each property having
> 16-bit address and 16-bit value. At least a portion of a chip
> configuration is done by modifying those properties. In order to

Right, that's what I remembered from previous code.  There's no way this
should be a regmap bus - a bus is something that gets data serialised by
the core into a byte stream, having the data rendered down into a byte
stream and then reparsing it is a bit silly.  The device should be
hooking in before the data gets marshalled which we can't currently do
but it shouldn't be too hard to make it so that we can have register
read and write functions supplied in the regmap config.

> Also due to the way the driver uses the chip it is only powered up when
> the corresponding file in devfs(e.g. /dev/radio0) is opened at least by
> one user which means that unless there is a user who opened the file all
> the SET/GET_PROPERTY commands sent to it will be lost. The codec driver
> for that chip does not have any say in the power management policy(while
> all the audio configuration is done via "properties") if the chip is not
> powered up the driver has to cache the configuration values it has so
> that they can be applied later.

This is very normal, indeed modern CODEC drivers can leave the chip
powered down whenever it's not performing some function.

> So, since I have to implement a caching functionality in the driver, in
> order to avoid reinventing the wheel I opted for using 'regmap' API for
> this.

> Of course, It is possible that I misunderstood the purpose and
> capabilities of the 'regmap' framework, which would make my code look
> very silly indeed. If that is the case I'll just re-implement it using
> some sort of ad-hoc version of caching.

No, what you're doing is totally sensible.  It needs a bit of extension
to the framework before you can do it though.
Andrey Smirnov Oct. 28, 2012, 2:08 a.m. UTC | #4
On 10/27/2012 02:31 PM, Mark Brown wrote:
> On Thu, Oct 25, 2012 at 03:26:02PM -0700, Andrey Smirnov wrote:
>> On 10/25/2012 12:45 PM, Mark Brown wrote:
>>> This really makes little sense to me, why are you doing this?  Does the
>>> device *really* layer a byte stream on top of I2C for sending messages
>>> that look like marshalled register reads and writes?
>> The SI476x chips has a concept of a "property". Each property having
>> 16-bit address and 16-bit value. At least a portion of a chip
>> configuration is done by modifying those properties. In order to
> Right, that's what I remembered from previous code.  There's no way this
> should be a regmap bus - a bus is something that gets data serialised by
> the core into a byte stream, having the data rendered down into a byte
> stream and then reparsing it is a bit silly.  The device should be
> hooking in before the data gets marshalled which we can't currently do
> but it shouldn't be too hard to make it so that we can have register
> read and write functions supplied in the regmap config.

Oh, now I think I see what you mean. I have two agree with you, I don't
think I like what I am doing in my code.
I'll try to familiarize myself with 'regmap' code and come up with the
way to extend the framework.

And I just wanted to upstream my simple radio driver... :-)



--
To unsubscribe from this list: send the line "unsubscribe linux-media" in
the body of a message to majordomo@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html
Hans Verkuil Nov. 16, 2012, 2:35 p.m. UTC | #5
Hi Andrey,

I'm really sorry for the long delay, but I finally have time to review v4 of this
code.

On Tue October 23 2012 20:44:28 Andrey Smirnov wrote:
> This patch adds main part(out of three) of the I2C driver for the
> "core" of MFD device.
> 
> Signed-off-by: Andrey Smirnov <andrey.smirnov@convergeddevices.net>
> ---
>  drivers/mfd/si476x-i2c.c |  966 ++++++++++++++++++++++++++++++++++++++++++++++
>  1 file changed, 966 insertions(+)
>  create mode 100644 drivers/mfd/si476x-i2c.c
> 
> diff --git a/drivers/mfd/si476x-i2c.c b/drivers/mfd/si476x-i2c.c
> new file mode 100644
> index 0000000..6d581bd
> --- /dev/null
> +++ b/drivers/mfd/si476x-i2c.c
> @@ -0,0 +1,966 @@
> +/*
> + * include/media/si476x-i2c.c -- Core device driver for si476x MFD
> + * device
> + *
> + * Copyright (C) 2012 Innovative Converged Devices(ICD)
> + *
> + * Author: Andrey Smirnov <andrey.smirnov@convergeddevices.net>
> + *
> + * This program is free software; you can redistribute it and/or modify
> + * it under the terms of the GNU General Public License as published by
> + * the Free Software Foundation; version 2 of the License.
> + *
> + * This program is distributed in the hope that it will be useful, but
> + * WITHOUT ANY WARRANTY; without even the implied warranty of
> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
> + * General Public License for more details.
> + *
> + */
> +#include <linux/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>
> +
> +/* Command Timeouts */
> +#define DEFAULT_TIMEOUT				100000
> +#define TIMEOUT_TUNE				700000
> +#define TIMEOUT_POWER_UP			330000
> +
> +#define MAX_IO_ERRORS 10
> +
> +#define SI476X_DRIVER_RDS_FIFO_DEPTH		128
> +
> +#define SI476X_STATUS_POLL_US 0
> +
> +/**
> + * 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(atomic_read(&core->polling_interval)));
> +}
> +
> +/**
> + * 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_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));
> +			DBG_BUFFER(&core->client->dev, "RDS data:\n",
> +				   report.rds, sizeof(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++ > MAX_IO_ERRORS)
> +			si476x_core_pronounce_dead(core);
> +	} else {
> +		io_errors_count = 0;
> +	}
> +
> +	return err;
> +}
> +EXPORT_SYMBOL_GPL(si476x_core_i2c_xfer);
> +
> +/**
> + * si476x_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_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_firmware_version_to_revision()
> + * @core: Core device structure
> + * @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 inline int si476x_core_firmware_version_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:		/* FALLTHROUG */
> +		BUG();
> +		return -1;
> +	}
> +
> +unknown_revision:
> +	dev_err(&core->client->dev,
> +		"Unsupported version of the firmware: %d.%d.%d, "
> +		"reverting to A10 comptible functions\n",
> +		major, minor1, minor2);
> +
> +	return SI476X_REVISION_A10;
> +}
> +
> +/**
> + * si476x_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) {
> +		rval = si476x_core_cmd_func_info(core, &info);
> +		if (!rval)
> +			core->revision = \
> +				si476x_core_firmware_version_to_revision(core,
> +									 info.func,
> +									 info.firmware.major,
> +									 info.firmware.minor[0],
> +									 info.firmware.minor[1]);
> +		si476x_core_set_power_state(core,
> +					    SI476X_POWER_DOWN);
> +	}
> +
> +	si476x_core_unlock(core);
> +
> +	return rval;
> +}
> +
> +#define ATOMIC_CORE_DEV_ATTR(__attr_name, __field_name)			\
> +	static ssize_t __attr_name##_show(struct device *dev,		\
> +					  struct device_attribute *attr, \
> +					  char *buf)			\
> +	{								\
> +		struct i2c_client  *client;				\
> +		struct si476x_core *core;				\
> +									\
> +		client = container_of(dev, struct i2c_client, dev);	\
> +		core   = i2c_get_clientdata(client);			\
> +									\
> +		return sprintf(buf, "%u", atomic_read(&core->__field_name)); \
> +	}								\
> +	static ssize_t __attr_name##_store(struct device *dev,		\
> +					   struct device_attribute *attr, \
> +					   const char *buf, size_t count) \
> +	{								\
> +		unsigned int delay;					\
> +									\
> +		struct i2c_client  *client;				\
> +		struct si476x_core *core;				\
> +									\
> +		if (sscanf(buf, "%u", &delay) != 1)			\
> +			return -EINVAL;					\
> +									\
> +		client = container_of(dev, struct i2c_client, dev);	\
> +		core   = i2c_get_clientdata(client);			\
> +									\
> +		atomic_set(&core->__field_name, delay);			\
> +		return count;						\
> +	}								\
> +	static DEVICE_ATTR(__attr_name, S_IWUSR|S_IRUGO,		\
> +			   __attr_name##_show, __attr_name##_store)
> +
> +
> +ATOMIC_CORE_DEV_ATTR(polling_interval_us, polling_interval);
> +ATOMIC_CORE_DEV_ATTR(tune_timeout_us, timeouts.tune);
> +ATOMIC_CORE_DEV_ATTR(command_timeout_us, timeouts.command);
> +ATOMIC_CORE_DEV_ATTR(power_up_timeout_us, timeouts.power_up);

Are these attrs really needed? And if so, are they documented somewhere?

I am always somewhat suspicious of exposing such parameters. It is my believe
that that is something the driver should just do correctly.

> +
> +static struct attribute *si476x_core_attrs[] = {
> +	&dev_attr_polling_interval_us.attr,
> +	&dev_attr_tune_timeout_us.attr,
> +	&dev_attr_command_timeout_us.attr,
> +	&dev_attr_power_up_timeout_us.attr,
> +	NULL
> +};
> +
> +static struct attribute_group si476x_core_attr_group = {
> +	.attrs = si476x_core_attrs,
> +};
> +
> +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);
> +
> +static int __devinit si476x_core_probe(struct i2c_client *client,
> +				       const struct i2c_device_id *id)
> +{
> +	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) {
> +		dev_err(&client->dev, "failed to allocate 'struct si476x_core'\n");
> +		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 = client->dev.platform_data;
> +	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, "Failet to gett 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 alloate 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);
> +
> +	atomic_set(&core->polling_interval, SI476X_STATUS_POLL_US);
> +
> +	atomic_set(&core->timeouts.tune, TIMEOUT_TUNE);
> +	atomic_set(&core->timeouts.power_up, TIMEOUT_POWER_UP);
> +	atomic_set(&core->timeouts.command, DEFAULT_TIMEOUT);
> +
> +	rval = sysfs_create_group(&client->dev.kobj, &si476x_core_attr_group);
> +	if (rval < 0) {
> +		dev_err(&client->dev, "Failed to create sysfs attributes\n");
> +		goto free_kfifo;
> +	}
> +
> +	if (client->irq) {
> +		rval = devm_request_threaded_irq(&client->dev,
> +						 client->irq, NULL, si476x_core_interrupt,
> +						 IRQF_TRIGGER_FALLING,
> +						 client->name, core);
> +		if (rval < 0) {
> +			dev_err(&client->dev, "Could not request IRQ %d\n",
> +				client->irq);
> +			goto free_sysfs;
> +		}
> +		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_sysfs;
> +	}
> +
> +	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_sysfs:
> +	sysfs_remove_group(&client->dev.kobj, &si476x_core_attr_group);
> +free_kfifo:
> +	kfifo_free(&core->rds_fifo);
> +
> +free_gpio:
> +	if (gpio_is_valid(core->gpio_reset))
> +		gpio_free(core->gpio_reset);
> +
> +	return rval;
> +}
> +
> +static int 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);
> +	}
> +
> +	sysfs_remove_group(&client->dev.kobj, &si476x_core_attr_group);
> +
> +	kfifo_free(&core->rds_fifo);
> +
> +	if (gpio_is_valid(core->gpio_reset))
> +		gpio_free(core->gpio_reset);
> +
> +	return 0;
> +}
> +
> +
> +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",
> +		.owner  = THIS_MODULE,
> +	},
> +	.probe		= si476x_core_probe,
> +	.remove         = __devexit_p(si476x_core_remove),
> +	.id_table       = si476x_id,
> +};
> +
> +static int __init si476x_core_init(void)
> +{
> +	return i2c_add_driver(&si476x_core_driver);
> +}
> +
> +static void __exit si476x_core_exit(void)
> +{
> +	i2c_del_driver(&si476x_core_driver);
> +}
> +late_initcall(si476x_core_init);
> +module_exit(si476x_core_exit);
> +
> +
> +MODULE_AUTHOR("Andrey Smirnov <andrey.smirnov@convergeddevices.net>");
> +MODULE_DESCRIPTION("Si4761/64/68 AM/FM MFD core device driver");
> +MODULE_LICENSE("GPL");
> 

Regards,

	Hans
--
To unsubscribe from this list: send the line "unsubscribe linux-media" in
the body of a message to majordomo@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html
diff mbox

Patch

diff --git a/drivers/mfd/si476x-i2c.c b/drivers/mfd/si476x-i2c.c
new file mode 100644
index 0000000..6d581bd
--- /dev/null
+++ b/drivers/mfd/si476x-i2c.c
@@ -0,0 +1,966 @@ 
+/*
+ * include/media/si476x-i2c.c -- Core device driver for si476x MFD
+ * device
+ *
+ * Copyright (C) 2012 Innovative Converged Devices(ICD)
+ *
+ * Author: Andrey Smirnov <andrey.smirnov@convergeddevices.net>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ */
+#include <linux/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>
+
+/* Command Timeouts */
+#define DEFAULT_TIMEOUT				100000
+#define TIMEOUT_TUNE				700000
+#define TIMEOUT_POWER_UP			330000
+
+#define MAX_IO_ERRORS 10
+
+#define SI476X_DRIVER_RDS_FIFO_DEPTH		128
+
+#define SI476X_STATUS_POLL_US 0
+
+/**
+ * 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(atomic_read(&core->polling_interval)));
+}
+
+/**
+ * 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_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));
+			DBG_BUFFER(&core->client->dev, "RDS data:\n",
+				   report.rds, sizeof(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++ > MAX_IO_ERRORS)
+			si476x_core_pronounce_dead(core);
+	} else {
+		io_errors_count = 0;
+	}
+
+	return err;
+}
+EXPORT_SYMBOL_GPL(si476x_core_i2c_xfer);
+
+/**
+ * si476x_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_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_firmware_version_to_revision()
+ * @core: Core device structure
+ * @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 inline int si476x_core_firmware_version_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:		/* FALLTHROUG */
+		BUG();
+		return -1;
+	}
+
+unknown_revision:
+	dev_err(&core->client->dev,
+		"Unsupported version of the firmware: %d.%d.%d, "
+		"reverting to A10 comptible functions\n",
+		major, minor1, minor2);
+
+	return SI476X_REVISION_A10;
+}
+
+/**
+ * si476x_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) {
+		rval = si476x_core_cmd_func_info(core, &info);
+		if (!rval)
+			core->revision = \
+				si476x_core_firmware_version_to_revision(core,
+									 info.func,
+									 info.firmware.major,
+									 info.firmware.minor[0],
+									 info.firmware.minor[1]);
+		si476x_core_set_power_state(core,
+					    SI476X_POWER_DOWN);
+	}
+
+	si476x_core_unlock(core);
+
+	return rval;
+}
+
+#define ATOMIC_CORE_DEV_ATTR(__attr_name, __field_name)			\
+	static ssize_t __attr_name##_show(struct device *dev,		\
+					  struct device_attribute *attr, \
+					  char *buf)			\
+	{								\
+		struct i2c_client  *client;				\
+		struct si476x_core *core;				\
+									\
+		client = container_of(dev, struct i2c_client, dev);	\
+		core   = i2c_get_clientdata(client);			\
+									\
+		return sprintf(buf, "%u", atomic_read(&core->__field_name)); \
+	}								\
+	static ssize_t __attr_name##_store(struct device *dev,		\
+					   struct device_attribute *attr, \
+					   const char *buf, size_t count) \
+	{								\
+		unsigned int delay;					\
+									\
+		struct i2c_client  *client;				\
+		struct si476x_core *core;				\
+									\
+		if (sscanf(buf, "%u", &delay) != 1)			\
+			return -EINVAL;					\
+									\
+		client = container_of(dev, struct i2c_client, dev);	\
+		core   = i2c_get_clientdata(client);			\
+									\
+		atomic_set(&core->__field_name, delay);			\
+		return count;						\
+	}								\
+	static DEVICE_ATTR(__attr_name, S_IWUSR|S_IRUGO,		\
+			   __attr_name##_show, __attr_name##_store)
+
+
+ATOMIC_CORE_DEV_ATTR(polling_interval_us, polling_interval);
+ATOMIC_CORE_DEV_ATTR(tune_timeout_us, timeouts.tune);
+ATOMIC_CORE_DEV_ATTR(command_timeout_us, timeouts.command);
+ATOMIC_CORE_DEV_ATTR(power_up_timeout_us, timeouts.power_up);
+
+static struct attribute *si476x_core_attrs[] = {
+	&dev_attr_polling_interval_us.attr,
+	&dev_attr_tune_timeout_us.attr,
+	&dev_attr_command_timeout_us.attr,
+	&dev_attr_power_up_timeout_us.attr,
+	NULL
+};
+
+static struct attribute_group si476x_core_attr_group = {
+	.attrs = si476x_core_attrs,
+};
+
+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);
+
+static int __devinit si476x_core_probe(struct i2c_client *client,
+				       const struct i2c_device_id *id)
+{
+	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) {
+		dev_err(&client->dev, "failed to allocate 'struct si476x_core'\n");
+		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 = client->dev.platform_data;
+	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, "Failet to gett 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 alloate 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);
+
+	atomic_set(&core->polling_interval, SI476X_STATUS_POLL_US);
+
+	atomic_set(&core->timeouts.tune, TIMEOUT_TUNE);
+	atomic_set(&core->timeouts.power_up, TIMEOUT_POWER_UP);
+	atomic_set(&core->timeouts.command, DEFAULT_TIMEOUT);
+
+	rval = sysfs_create_group(&client->dev.kobj, &si476x_core_attr_group);
+	if (rval < 0) {
+		dev_err(&client->dev, "Failed to create sysfs attributes\n");
+		goto free_kfifo;
+	}
+
+	if (client->irq) {
+		rval = devm_request_threaded_irq(&client->dev,
+						 client->irq, NULL, si476x_core_interrupt,
+						 IRQF_TRIGGER_FALLING,
+						 client->name, core);
+		if (rval < 0) {
+			dev_err(&client->dev, "Could not request IRQ %d\n",
+				client->irq);
+			goto free_sysfs;
+		}
+		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_sysfs;
+	}
+
+	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_sysfs:
+	sysfs_remove_group(&client->dev.kobj, &si476x_core_attr_group);
+free_kfifo:
+	kfifo_free(&core->rds_fifo);
+
+free_gpio:
+	if (gpio_is_valid(core->gpio_reset))
+		gpio_free(core->gpio_reset);
+
+	return rval;
+}
+
+static int 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);
+	}
+
+	sysfs_remove_group(&client->dev.kobj, &si476x_core_attr_group);
+
+	kfifo_free(&core->rds_fifo);
+
+	if (gpio_is_valid(core->gpio_reset))
+		gpio_free(core->gpio_reset);
+
+	return 0;
+}
+
+
+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",
+		.owner  = THIS_MODULE,
+	},
+	.probe		= si476x_core_probe,
+	.remove         = __devexit_p(si476x_core_remove),
+	.id_table       = si476x_id,
+};
+
+static int __init si476x_core_init(void)
+{
+	return i2c_add_driver(&si476x_core_driver);
+}
+
+static void __exit si476x_core_exit(void)
+{
+	i2c_del_driver(&si476x_core_driver);
+}
+late_initcall(si476x_core_init);
+module_exit(si476x_core_exit);
+
+
+MODULE_AUTHOR("Andrey Smirnov <andrey.smirnov@convergeddevices.net>");
+MODULE_DESCRIPTION("Si4761/64/68 AM/FM MFD core device driver");
+MODULE_LICENSE("GPL");