1381 lines
30 KiB
C
1381 lines
30 KiB
C
/*
|
|
* Copyright (C) 2007-2010 ST-Ericsson
|
|
* License terms: GNU General Public License (GPL) version 2
|
|
* Low-level core for exclusive access to the AB3550 IC on the I2C bus
|
|
* and some basic chip-configuration.
|
|
* Author: Bengt Jonsson <bengt.g.jonsson@stericsson.com>
|
|
* Author: Mattias Nilsson <mattias.i.nilsson@stericsson.com>
|
|
* Author: Mattias Wallin <mattias.wallin@stericsson.com>
|
|
* Author: Rickard Andersson <rickard.andersson@stericsson.com>
|
|
*/
|
|
|
|
#include <linux/i2c.h>
|
|
#include <linux/mutex.h>
|
|
#include <linux/err.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/device.h>
|
|
#include <linux/irq.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/random.h>
|
|
#include <linux/workqueue.h>
|
|
#include <linux/debugfs.h>
|
|
#include <linux/seq_file.h>
|
|
#include <linux/uaccess.h>
|
|
#include <linux/mfd/abx500.h>
|
|
#include <linux/list.h>
|
|
#include <linux/bitops.h>
|
|
#include <linux/spinlock.h>
|
|
#include <linux/mfd/core.h>
|
|
|
|
#define AB3550_NAME_STRING "ab3550"
|
|
#define AB3550_ID_FORMAT_STRING "AB3550 %s"
|
|
#define AB3550_NUM_BANKS 2
|
|
#define AB3550_NUM_EVENT_REG 5
|
|
|
|
/* These are the only registers inside AB3550 used in this main file */
|
|
|
|
/* Chip ID register */
|
|
#define AB3550_CID_REG 0x20
|
|
|
|
/* Interrupt event registers */
|
|
#define AB3550_EVENT_BANK 0
|
|
#define AB3550_EVENT_REG 0x22
|
|
|
|
/* Read/write operation values. */
|
|
#define AB3550_PERM_RD (0x01)
|
|
#define AB3550_PERM_WR (0x02)
|
|
|
|
/* Read/write permissions. */
|
|
#define AB3550_PERM_RO (AB3550_PERM_RD)
|
|
#define AB3550_PERM_RW (AB3550_PERM_RD | AB3550_PERM_WR)
|
|
|
|
/**
|
|
* struct ab3550
|
|
* @access_mutex: lock out concurrent accesses to the AB registers
|
|
* @i2c_client: I2C client for this chip
|
|
* @chip_name: name of this chip variant
|
|
* @chip_id: 8 bit chip ID for this chip variant
|
|
* @mask_work: a worker for writing to mask registers
|
|
* @event_lock: a lock to protect the event_mask
|
|
* @event_mask: a local copy of the mask event registers
|
|
* @startup_events: a copy of the first reading of the event registers
|
|
* @startup_events_read: whether the first events have been read
|
|
*/
|
|
struct ab3550 {
|
|
struct mutex access_mutex;
|
|
struct i2c_client *i2c_client[AB3550_NUM_BANKS];
|
|
char chip_name[32];
|
|
u8 chip_id;
|
|
struct work_struct mask_work;
|
|
spinlock_t event_lock;
|
|
u8 event_mask[AB3550_NUM_EVENT_REG];
|
|
u8 startup_events[AB3550_NUM_EVENT_REG];
|
|
bool startup_events_read;
|
|
#ifdef CONFIG_DEBUG_FS
|
|
unsigned int debug_bank;
|
|
unsigned int debug_address;
|
|
#endif
|
|
};
|
|
|
|
/**
|
|
* struct ab3550_reg_range
|
|
* @first: the first address of the range
|
|
* @last: the last address of the range
|
|
* @perm: access permissions for the range
|
|
*/
|
|
struct ab3550_reg_range {
|
|
u8 first;
|
|
u8 last;
|
|
u8 perm;
|
|
};
|
|
|
|
/**
|
|
* struct ab3550_reg_ranges
|
|
* @count: the number of ranges in the list
|
|
* @range: the list of register ranges
|
|
*/
|
|
struct ab3550_reg_ranges {
|
|
u8 count;
|
|
const struct ab3550_reg_range *range;
|
|
};
|
|
|
|
/*
|
|
* Permissible register ranges for reading and writing per device and bank.
|
|
*
|
|
* The ranges must be listed in increasing address order, and no overlaps are
|
|
* allowed. It is assumed that write permission implies read permission
|
|
* (i.e. only RO and RW permissions should be used). Ranges with write
|
|
* permission must not be split up.
|
|
*/
|
|
|
|
#define NO_RANGE {.count = 0, .range = NULL,}
|
|
|
|
static struct
|
|
ab3550_reg_ranges ab3550_reg_ranges[AB3550_NUM_DEVICES][AB3550_NUM_BANKS] = {
|
|
[AB3550_DEVID_DAC] = {
|
|
NO_RANGE,
|
|
{
|
|
.count = 2,
|
|
.range = (struct ab3550_reg_range[]) {
|
|
{
|
|
.first = 0xb0,
|
|
.last = 0xba,
|
|
.perm = AB3550_PERM_RW,
|
|
},
|
|
{
|
|
.first = 0xbc,
|
|
.last = 0xc3,
|
|
.perm = AB3550_PERM_RW,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
[AB3550_DEVID_LEDS] = {
|
|
NO_RANGE,
|
|
{
|
|
.count = 2,
|
|
.range = (struct ab3550_reg_range[]) {
|
|
{
|
|
.first = 0x5a,
|
|
.last = 0x88,
|
|
.perm = AB3550_PERM_RW,
|
|
},
|
|
{
|
|
.first = 0x8a,
|
|
.last = 0xad,
|
|
.perm = AB3550_PERM_RW,
|
|
},
|
|
}
|
|
},
|
|
},
|
|
[AB3550_DEVID_POWER] = {
|
|
{
|
|
.count = 1,
|
|
.range = (struct ab3550_reg_range[]) {
|
|
{
|
|
.first = 0x21,
|
|
.last = 0x21,
|
|
.perm = AB3550_PERM_RO,
|
|
},
|
|
}
|
|
},
|
|
NO_RANGE,
|
|
},
|
|
[AB3550_DEVID_REGULATORS] = {
|
|
{
|
|
.count = 1,
|
|
.range = (struct ab3550_reg_range[]) {
|
|
{
|
|
.first = 0x69,
|
|
.last = 0xa3,
|
|
.perm = AB3550_PERM_RW,
|
|
},
|
|
}
|
|
},
|
|
{
|
|
.count = 1,
|
|
.range = (struct ab3550_reg_range[]) {
|
|
{
|
|
.first = 0x14,
|
|
.last = 0x16,
|
|
.perm = AB3550_PERM_RW,
|
|
},
|
|
}
|
|
},
|
|
},
|
|
[AB3550_DEVID_SIM] = {
|
|
{
|
|
.count = 1,
|
|
.range = (struct ab3550_reg_range[]) {
|
|
{
|
|
.first = 0x21,
|
|
.last = 0x21,
|
|
.perm = AB3550_PERM_RO,
|
|
},
|
|
}
|
|
},
|
|
{
|
|
.count = 1,
|
|
.range = (struct ab3550_reg_range[]) {
|
|
{
|
|
.first = 0x14,
|
|
.last = 0x17,
|
|
.perm = AB3550_PERM_RW,
|
|
},
|
|
}
|
|
|
|
},
|
|
},
|
|
[AB3550_DEVID_UART] = {
|
|
NO_RANGE,
|
|
NO_RANGE,
|
|
},
|
|
[AB3550_DEVID_RTC] = {
|
|
{
|
|
.count = 1,
|
|
.range = (struct ab3550_reg_range[]) {
|
|
{
|
|
.first = 0x00,
|
|
.last = 0x0c,
|
|
.perm = AB3550_PERM_RW,
|
|
},
|
|
}
|
|
},
|
|
NO_RANGE,
|
|
},
|
|
[AB3550_DEVID_CHARGER] = {
|
|
{
|
|
.count = 2,
|
|
.range = (struct ab3550_reg_range[]) {
|
|
{
|
|
.first = 0x10,
|
|
.last = 0x1a,
|
|
.perm = AB3550_PERM_RW,
|
|
},
|
|
{
|
|
.first = 0x21,
|
|
.last = 0x21,
|
|
.perm = AB3550_PERM_RO,
|
|
},
|
|
}
|
|
},
|
|
NO_RANGE,
|
|
},
|
|
[AB3550_DEVID_ADC] = {
|
|
NO_RANGE,
|
|
{
|
|
.count = 1,
|
|
.range = (struct ab3550_reg_range[]) {
|
|
{
|
|
.first = 0x20,
|
|
.last = 0x56,
|
|
.perm = AB3550_PERM_RW,
|
|
},
|
|
|
|
}
|
|
},
|
|
},
|
|
[AB3550_DEVID_FUELGAUGE] = {
|
|
{
|
|
.count = 1,
|
|
.range = (struct ab3550_reg_range[]) {
|
|
{
|
|
.first = 0x21,
|
|
.last = 0x21,
|
|
.perm = AB3550_PERM_RO,
|
|
},
|
|
}
|
|
},
|
|
{
|
|
.count = 1,
|
|
.range = (struct ab3550_reg_range[]) {
|
|
{
|
|
.first = 0x00,
|
|
.last = 0x0e,
|
|
.perm = AB3550_PERM_RW,
|
|
},
|
|
}
|
|
},
|
|
},
|
|
[AB3550_DEVID_VIBRATOR] = {
|
|
NO_RANGE,
|
|
{
|
|
.count = 1,
|
|
.range = (struct ab3550_reg_range[]) {
|
|
{
|
|
.first = 0x10,
|
|
.last = 0x13,
|
|
.perm = AB3550_PERM_RW,
|
|
},
|
|
|
|
}
|
|
},
|
|
},
|
|
[AB3550_DEVID_CODEC] = {
|
|
{
|
|
.count = 2,
|
|
.range = (struct ab3550_reg_range[]) {
|
|
{
|
|
.first = 0x31,
|
|
.last = 0x63,
|
|
.perm = AB3550_PERM_RW,
|
|
},
|
|
{
|
|
.first = 0x65,
|
|
.last = 0x68,
|
|
.perm = AB3550_PERM_RW,
|
|
},
|
|
}
|
|
},
|
|
NO_RANGE,
|
|
},
|
|
};
|
|
|
|
static struct mfd_cell ab3550_devs[AB3550_NUM_DEVICES] = {
|
|
[AB3550_DEVID_DAC] = {
|
|
.name = "ab3550-dac",
|
|
.id = AB3550_DEVID_DAC,
|
|
.num_resources = 0,
|
|
},
|
|
[AB3550_DEVID_LEDS] = {
|
|
.name = "ab3550-leds",
|
|
.id = AB3550_DEVID_LEDS,
|
|
},
|
|
[AB3550_DEVID_POWER] = {
|
|
.name = "ab3550-power",
|
|
.id = AB3550_DEVID_POWER,
|
|
},
|
|
[AB3550_DEVID_REGULATORS] = {
|
|
.name = "ab3550-regulators",
|
|
.id = AB3550_DEVID_REGULATORS,
|
|
},
|
|
[AB3550_DEVID_SIM] = {
|
|
.name = "ab3550-sim",
|
|
.id = AB3550_DEVID_SIM,
|
|
},
|
|
[AB3550_DEVID_UART] = {
|
|
.name = "ab3550-uart",
|
|
.id = AB3550_DEVID_UART,
|
|
},
|
|
[AB3550_DEVID_RTC] = {
|
|
.name = "ab3550-rtc",
|
|
.id = AB3550_DEVID_RTC,
|
|
},
|
|
[AB3550_DEVID_CHARGER] = {
|
|
.name = "ab3550-charger",
|
|
.id = AB3550_DEVID_CHARGER,
|
|
},
|
|
[AB3550_DEVID_ADC] = {
|
|
.name = "ab3550-adc",
|
|
.id = AB3550_DEVID_ADC,
|
|
.num_resources = 10,
|
|
.resources = (struct resource[]) {
|
|
{
|
|
.name = "TRIGGER-0",
|
|
.flags = IORESOURCE_IRQ,
|
|
.start = 16,
|
|
.end = 16,
|
|
},
|
|
{
|
|
.name = "TRIGGER-1",
|
|
.flags = IORESOURCE_IRQ,
|
|
.start = 17,
|
|
.end = 17,
|
|
},
|
|
{
|
|
.name = "TRIGGER-2",
|
|
.flags = IORESOURCE_IRQ,
|
|
.start = 18,
|
|
.end = 18,
|
|
},
|
|
{
|
|
.name = "TRIGGER-3",
|
|
.flags = IORESOURCE_IRQ,
|
|
.start = 19,
|
|
.end = 19,
|
|
},
|
|
{
|
|
.name = "TRIGGER-4",
|
|
.flags = IORESOURCE_IRQ,
|
|
.start = 20,
|
|
.end = 20,
|
|
},
|
|
{
|
|
.name = "TRIGGER-5",
|
|
.flags = IORESOURCE_IRQ,
|
|
.start = 21,
|
|
.end = 21,
|
|
},
|
|
{
|
|
.name = "TRIGGER-6",
|
|
.flags = IORESOURCE_IRQ,
|
|
.start = 22,
|
|
.end = 22,
|
|
},
|
|
{
|
|
.name = "TRIGGER-7",
|
|
.flags = IORESOURCE_IRQ,
|
|
.start = 23,
|
|
.end = 23,
|
|
},
|
|
{
|
|
.name = "TRIGGER-VBAT-TXON",
|
|
.flags = IORESOURCE_IRQ,
|
|
.start = 13,
|
|
.end = 13,
|
|
},
|
|
{
|
|
.name = "TRIGGER-VBAT",
|
|
.flags = IORESOURCE_IRQ,
|
|
.start = 12,
|
|
.end = 12,
|
|
},
|
|
},
|
|
},
|
|
[AB3550_DEVID_FUELGAUGE] = {
|
|
.name = "ab3550-fuelgauge",
|
|
.id = AB3550_DEVID_FUELGAUGE,
|
|
},
|
|
[AB3550_DEVID_VIBRATOR] = {
|
|
.name = "ab3550-vibrator",
|
|
.id = AB3550_DEVID_VIBRATOR,
|
|
},
|
|
[AB3550_DEVID_CODEC] = {
|
|
.name = "ab3550-codec",
|
|
.id = AB3550_DEVID_CODEC,
|
|
},
|
|
};
|
|
|
|
/*
|
|
* I2C transactions with error messages.
|
|
*/
|
|
static int ab3550_i2c_master_send(struct ab3550 *ab, u8 bank, u8 *data,
|
|
u8 count)
|
|
{
|
|
int err;
|
|
|
|
err = i2c_master_send(ab->i2c_client[bank], data, count);
|
|
if (err < 0) {
|
|
dev_err(&ab->i2c_client[0]->dev, "send error: %d\n", err);
|
|
return err;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int ab3550_i2c_master_recv(struct ab3550 *ab, u8 bank, u8 *data,
|
|
u8 count)
|
|
{
|
|
int err;
|
|
|
|
err = i2c_master_recv(ab->i2c_client[bank], data, count);
|
|
if (err < 0) {
|
|
dev_err(&ab->i2c_client[0]->dev, "receive error: %d\n", err);
|
|
return err;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Functionality for getting/setting register values.
|
|
*/
|
|
static int get_register_interruptible(struct ab3550 *ab, u8 bank, u8 reg,
|
|
u8 *value)
|
|
{
|
|
int err;
|
|
|
|
err = mutex_lock_interruptible(&ab->access_mutex);
|
|
if (err)
|
|
return err;
|
|
|
|
err = ab3550_i2c_master_send(ab, bank, ®, 1);
|
|
if (!err)
|
|
err = ab3550_i2c_master_recv(ab, bank, value, 1);
|
|
|
|
mutex_unlock(&ab->access_mutex);
|
|
return err;
|
|
}
|
|
|
|
static int get_register_page_interruptible(struct ab3550 *ab, u8 bank,
|
|
u8 first_reg, u8 *regvals, u8 numregs)
|
|
{
|
|
int err;
|
|
|
|
err = mutex_lock_interruptible(&ab->access_mutex);
|
|
if (err)
|
|
return err;
|
|
|
|
err = ab3550_i2c_master_send(ab, bank, &first_reg, 1);
|
|
if (!err)
|
|
err = ab3550_i2c_master_recv(ab, bank, regvals, numregs);
|
|
|
|
mutex_unlock(&ab->access_mutex);
|
|
return err;
|
|
}
|
|
|
|
static int mask_and_set_register_interruptible(struct ab3550 *ab, u8 bank,
|
|
u8 reg, u8 bitmask, u8 bitvalues)
|
|
{
|
|
int err = 0;
|
|
|
|
if (likely(bitmask)) {
|
|
u8 reg_bits[2] = {reg, 0};
|
|
|
|
err = mutex_lock_interruptible(&ab->access_mutex);
|
|
if (err)
|
|
return err;
|
|
|
|
if (bitmask == 0xFF) /* No need to read in this case. */
|
|
reg_bits[1] = bitvalues;
|
|
else { /* Read and modify the register value. */
|
|
u8 bits;
|
|
|
|
err = ab3550_i2c_master_send(ab, bank, ®, 1);
|
|
if (err)
|
|
goto unlock_and_return;
|
|
err = ab3550_i2c_master_recv(ab, bank, &bits, 1);
|
|
if (err)
|
|
goto unlock_and_return;
|
|
reg_bits[1] = ((~bitmask & bits) |
|
|
(bitmask & bitvalues));
|
|
}
|
|
/* Write the new value. */
|
|
err = ab3550_i2c_master_send(ab, bank, reg_bits, 2);
|
|
unlock_and_return:
|
|
mutex_unlock(&ab->access_mutex);
|
|
}
|
|
return err;
|
|
}
|
|
|
|
/*
|
|
* Read/write permission checking functions.
|
|
*/
|
|
static bool page_write_allowed(const struct ab3550_reg_ranges *ranges,
|
|
u8 first_reg, u8 last_reg)
|
|
{
|
|
u8 i;
|
|
|
|
if (last_reg < first_reg)
|
|
return false;
|
|
|
|
for (i = 0; i < ranges->count; i++) {
|
|
if (first_reg < ranges->range[i].first)
|
|
break;
|
|
if ((last_reg <= ranges->range[i].last) &&
|
|
(ranges->range[i].perm & AB3550_PERM_WR))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool reg_write_allowed(const struct ab3550_reg_ranges *ranges, u8 reg)
|
|
{
|
|
return page_write_allowed(ranges, reg, reg);
|
|
}
|
|
|
|
static bool page_read_allowed(const struct ab3550_reg_ranges *ranges,
|
|
u8 first_reg, u8 last_reg)
|
|
{
|
|
u8 i;
|
|
|
|
if (last_reg < first_reg)
|
|
return false;
|
|
/* Find the range (if it exists in the list) that includes first_reg. */
|
|
for (i = 0; i < ranges->count; i++) {
|
|
if (first_reg < ranges->range[i].first)
|
|
return false;
|
|
if (first_reg <= ranges->range[i].last)
|
|
break;
|
|
}
|
|
/* Make sure that the entire range up to and including last_reg is
|
|
* readable. This may span several of the ranges in the list.
|
|
*/
|
|
while ((i < ranges->count) &&
|
|
(ranges->range[i].perm & AB3550_PERM_RD)) {
|
|
if (last_reg <= ranges->range[i].last)
|
|
return true;
|
|
if ((++i >= ranges->count) ||
|
|
(ranges->range[i].first !=
|
|
(ranges->range[i - 1].last + 1))) {
|
|
break;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool reg_read_allowed(const struct ab3550_reg_ranges *ranges, u8 reg)
|
|
{
|
|
return page_read_allowed(ranges, reg, reg);
|
|
}
|
|
|
|
/*
|
|
* The register access functionality.
|
|
*/
|
|
static int ab3550_get_chip_id(struct device *dev)
|
|
{
|
|
struct ab3550 *ab = dev_get_drvdata(dev->parent);
|
|
return (int)ab->chip_id;
|
|
}
|
|
|
|
static int ab3550_mask_and_set_register_interruptible(struct device *dev,
|
|
u8 bank, u8 reg, u8 bitmask, u8 bitvalues)
|
|
{
|
|
struct ab3550 *ab;
|
|
struct platform_device *pdev = to_platform_device(dev);
|
|
|
|
if ((AB3550_NUM_BANKS <= bank) ||
|
|
!reg_write_allowed(&ab3550_reg_ranges[pdev->id][bank], reg))
|
|
return -EINVAL;
|
|
|
|
ab = dev_get_drvdata(dev->parent);
|
|
return mask_and_set_register_interruptible(ab, bank, reg,
|
|
bitmask, bitvalues);
|
|
}
|
|
|
|
static int ab3550_set_register_interruptible(struct device *dev, u8 bank,
|
|
u8 reg, u8 value)
|
|
{
|
|
return ab3550_mask_and_set_register_interruptible(dev, bank, reg, 0xFF,
|
|
value);
|
|
}
|
|
|
|
static int ab3550_get_register_interruptible(struct device *dev, u8 bank,
|
|
u8 reg, u8 *value)
|
|
{
|
|
struct ab3550 *ab;
|
|
struct platform_device *pdev = to_platform_device(dev);
|
|
|
|
if ((AB3550_NUM_BANKS <= bank) ||
|
|
!reg_read_allowed(&ab3550_reg_ranges[pdev->id][bank], reg))
|
|
return -EINVAL;
|
|
|
|
ab = dev_get_drvdata(dev->parent);
|
|
return get_register_interruptible(ab, bank, reg, value);
|
|
}
|
|
|
|
static int ab3550_get_register_page_interruptible(struct device *dev, u8 bank,
|
|
u8 first_reg, u8 *regvals, u8 numregs)
|
|
{
|
|
struct ab3550 *ab;
|
|
struct platform_device *pdev = to_platform_device(dev);
|
|
|
|
if ((AB3550_NUM_BANKS <= bank) ||
|
|
!page_read_allowed(&ab3550_reg_ranges[pdev->id][bank],
|
|
first_reg, (first_reg + numregs - 1)))
|
|
return -EINVAL;
|
|
|
|
ab = dev_get_drvdata(dev->parent);
|
|
return get_register_page_interruptible(ab, bank, first_reg, regvals,
|
|
numregs);
|
|
}
|
|
|
|
static int ab3550_event_registers_startup_state_get(struct device *dev,
|
|
u8 *event)
|
|
{
|
|
struct ab3550 *ab;
|
|
|
|
ab = dev_get_drvdata(dev->parent);
|
|
if (!ab->startup_events_read)
|
|
return -EAGAIN; /* Try again later */
|
|
|
|
memcpy(event, ab->startup_events, AB3550_NUM_EVENT_REG);
|
|
return 0;
|
|
}
|
|
|
|
static int ab3550_startup_irq_enabled(struct device *dev, unsigned int irq)
|
|
{
|
|
struct ab3550 *ab;
|
|
struct ab3550_platform_data *plf_data;
|
|
bool val;
|
|
|
|
ab = irq_get_chip_data(irq);
|
|
plf_data = ab->i2c_client[0]->dev.platform_data;
|
|
irq -= plf_data->irq.base;
|
|
val = ((ab->startup_events[irq / 8] & BIT(irq % 8)) != 0);
|
|
|
|
return val;
|
|
}
|
|
|
|
static struct abx500_ops ab3550_ops = {
|
|
.get_chip_id = ab3550_get_chip_id,
|
|
.get_register = ab3550_get_register_interruptible,
|
|
.set_register = ab3550_set_register_interruptible,
|
|
.get_register_page = ab3550_get_register_page_interruptible,
|
|
.set_register_page = NULL,
|
|
.mask_and_set_register = ab3550_mask_and_set_register_interruptible,
|
|
.event_registers_startup_state_get =
|
|
ab3550_event_registers_startup_state_get,
|
|
.startup_irq_enabled = ab3550_startup_irq_enabled,
|
|
};
|
|
|
|
static irqreturn_t ab3550_irq_handler(int irq, void *data)
|
|
{
|
|
struct ab3550 *ab = data;
|
|
int err;
|
|
unsigned int i;
|
|
u8 e[AB3550_NUM_EVENT_REG];
|
|
u8 *events;
|
|
unsigned long flags;
|
|
|
|
events = (ab->startup_events_read ? e : ab->startup_events);
|
|
|
|
err = get_register_page_interruptible(ab, AB3550_EVENT_BANK,
|
|
AB3550_EVENT_REG, events, AB3550_NUM_EVENT_REG);
|
|
if (err)
|
|
goto err_event_rd;
|
|
|
|
if (!ab->startup_events_read) {
|
|
dev_info(&ab->i2c_client[0]->dev,
|
|
"startup events 0x%x,0x%x,0x%x,0x%x,0x%x\n",
|
|
ab->startup_events[0], ab->startup_events[1],
|
|
ab->startup_events[2], ab->startup_events[3],
|
|
ab->startup_events[4]);
|
|
ab->startup_events_read = true;
|
|
goto out;
|
|
}
|
|
|
|
/* The two highest bits in event[4] are not used. */
|
|
events[4] &= 0x3f;
|
|
|
|
spin_lock_irqsave(&ab->event_lock, flags);
|
|
for (i = 0; i < AB3550_NUM_EVENT_REG; i++)
|
|
events[i] &= ~ab->event_mask[i];
|
|
spin_unlock_irqrestore(&ab->event_lock, flags);
|
|
|
|
for (i = 0; i < AB3550_NUM_EVENT_REG; i++) {
|
|
u8 bit;
|
|
u8 event_reg;
|
|
|
|
dev_dbg(&ab->i2c_client[0]->dev, "IRQ Event[%d]: 0x%2x\n",
|
|
i, events[i]);
|
|
|
|
event_reg = events[i];
|
|
for (bit = 0; event_reg; bit++, event_reg /= 2) {
|
|
if (event_reg % 2) {
|
|
unsigned int irq;
|
|
struct ab3550_platform_data *plf_data;
|
|
|
|
plf_data = ab->i2c_client[0]->dev.platform_data;
|
|
irq = plf_data->irq.base + (i * 8) + bit;
|
|
handle_nested_irq(irq);
|
|
}
|
|
}
|
|
}
|
|
out:
|
|
return IRQ_HANDLED;
|
|
|
|
err_event_rd:
|
|
dev_dbg(&ab->i2c_client[0]->dev, "error reading event registers\n");
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
#ifdef CONFIG_DEBUG_FS
|
|
static struct ab3550_reg_ranges debug_ranges[AB3550_NUM_BANKS] = {
|
|
{
|
|
.count = 6,
|
|
.range = (struct ab3550_reg_range[]) {
|
|
{
|
|
.first = 0x00,
|
|
.last = 0x0e,
|
|
},
|
|
{
|
|
.first = 0x10,
|
|
.last = 0x1a,
|
|
},
|
|
{
|
|
.first = 0x1e,
|
|
.last = 0x4f,
|
|
},
|
|
{
|
|
.first = 0x51,
|
|
.last = 0x63,
|
|
},
|
|
{
|
|
.first = 0x65,
|
|
.last = 0xa3,
|
|
},
|
|
{
|
|
.first = 0xa5,
|
|
.last = 0xa8,
|
|
},
|
|
}
|
|
},
|
|
{
|
|
.count = 8,
|
|
.range = (struct ab3550_reg_range[]) {
|
|
{
|
|
.first = 0x00,
|
|
.last = 0x0e,
|
|
},
|
|
{
|
|
.first = 0x10,
|
|
.last = 0x17,
|
|
},
|
|
{
|
|
.first = 0x1a,
|
|
.last = 0x1c,
|
|
},
|
|
{
|
|
.first = 0x20,
|
|
.last = 0x56,
|
|
},
|
|
{
|
|
.first = 0x5a,
|
|
.last = 0x88,
|
|
},
|
|
{
|
|
.first = 0x8a,
|
|
.last = 0xad,
|
|
},
|
|
{
|
|
.first = 0xb0,
|
|
.last = 0xba,
|
|
},
|
|
{
|
|
.first = 0xbc,
|
|
.last = 0xc3,
|
|
},
|
|
}
|
|
},
|
|
};
|
|
|
|
static int ab3550_registers_print(struct seq_file *s, void *p)
|
|
{
|
|
struct ab3550 *ab = s->private;
|
|
int bank;
|
|
|
|
seq_printf(s, AB3550_NAME_STRING " register values:\n");
|
|
|
|
for (bank = 0; bank < AB3550_NUM_BANKS; bank++) {
|
|
unsigned int i;
|
|
|
|
seq_printf(s, " bank %d:\n", bank);
|
|
for (i = 0; i < debug_ranges[bank].count; i++) {
|
|
u8 reg;
|
|
|
|
for (reg = debug_ranges[bank].range[i].first;
|
|
reg <= debug_ranges[bank].range[i].last;
|
|
reg++) {
|
|
u8 value;
|
|
|
|
get_register_interruptible(ab, bank, reg,
|
|
&value);
|
|
seq_printf(s, " [%d/0x%02X]: 0x%02X\n", bank,
|
|
reg, value);
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int ab3550_registers_open(struct inode *inode, struct file *file)
|
|
{
|
|
return single_open(file, ab3550_registers_print, inode->i_private);
|
|
}
|
|
|
|
static const struct file_operations ab3550_registers_fops = {
|
|
.open = ab3550_registers_open,
|
|
.read = seq_read,
|
|
.llseek = seq_lseek,
|
|
.release = single_release,
|
|
.owner = THIS_MODULE,
|
|
};
|
|
|
|
static int ab3550_bank_print(struct seq_file *s, void *p)
|
|
{
|
|
struct ab3550 *ab = s->private;
|
|
|
|
seq_printf(s, "%d\n", ab->debug_bank);
|
|
return 0;
|
|
}
|
|
|
|
static int ab3550_bank_open(struct inode *inode, struct file *file)
|
|
{
|
|
return single_open(file, ab3550_bank_print, inode->i_private);
|
|
}
|
|
|
|
static ssize_t ab3550_bank_write(struct file *file,
|
|
const char __user *user_buf,
|
|
size_t count, loff_t *ppos)
|
|
{
|
|
struct ab3550 *ab = ((struct seq_file *)(file->private_data))->private;
|
|
unsigned long user_bank;
|
|
int err;
|
|
|
|
/* Get userspace string and assure termination */
|
|
err = kstrtoul_from_user(user_buf, count, 0, &user_bank);
|
|
if (err)
|
|
return err;
|
|
|
|
if (user_bank >= AB3550_NUM_BANKS) {
|
|
dev_err(&ab->i2c_client[0]->dev,
|
|
"debugfs error input > number of banks\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
ab->debug_bank = user_bank;
|
|
|
|
return count;
|
|
}
|
|
|
|
static int ab3550_address_print(struct seq_file *s, void *p)
|
|
{
|
|
struct ab3550 *ab = s->private;
|
|
|
|
seq_printf(s, "0x%02X\n", ab->debug_address);
|
|
return 0;
|
|
}
|
|
|
|
static int ab3550_address_open(struct inode *inode, struct file *file)
|
|
{
|
|
return single_open(file, ab3550_address_print, inode->i_private);
|
|
}
|
|
|
|
static ssize_t ab3550_address_write(struct file *file,
|
|
const char __user *user_buf,
|
|
size_t count, loff_t *ppos)
|
|
{
|
|
struct ab3550 *ab = ((struct seq_file *)(file->private_data))->private;
|
|
unsigned long user_address;
|
|
int err;
|
|
|
|
/* Get userspace string and assure termination */
|
|
err = kstrtoul_from_user(user_buf, count, 0, &user_address);
|
|
if (err)
|
|
return err;
|
|
|
|
if (user_address > 0xff) {
|
|
dev_err(&ab->i2c_client[0]->dev,
|
|
"debugfs error input > 0xff\n");
|
|
return -EINVAL;
|
|
}
|
|
ab->debug_address = user_address;
|
|
return count;
|
|
}
|
|
|
|
static int ab3550_val_print(struct seq_file *s, void *p)
|
|
{
|
|
struct ab3550 *ab = s->private;
|
|
int err;
|
|
u8 regvalue;
|
|
|
|
err = get_register_interruptible(ab, (u8)ab->debug_bank,
|
|
(u8)ab->debug_address, ®value);
|
|
if (err)
|
|
return -EINVAL;
|
|
seq_printf(s, "0x%02X\n", regvalue);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int ab3550_val_open(struct inode *inode, struct file *file)
|
|
{
|
|
return single_open(file, ab3550_val_print, inode->i_private);
|
|
}
|
|
|
|
static ssize_t ab3550_val_write(struct file *file,
|
|
const char __user *user_buf,
|
|
size_t count, loff_t *ppos)
|
|
{
|
|
struct ab3550 *ab = ((struct seq_file *)(file->private_data))->private;
|
|
unsigned long user_val;
|
|
int err;
|
|
u8 regvalue;
|
|
|
|
/* Get userspace string and assure termination */
|
|
err = kstrtoul_from_user(user_buf, count, 0, &user_val);
|
|
if (err)
|
|
return err;
|
|
|
|
if (user_val > 0xff) {
|
|
dev_err(&ab->i2c_client[0]->dev,
|
|
"debugfs error input > 0xff\n");
|
|
return -EINVAL;
|
|
}
|
|
err = mask_and_set_register_interruptible(
|
|
ab, (u8)ab->debug_bank,
|
|
(u8)ab->debug_address, 0xFF, (u8)user_val);
|
|
if (err)
|
|
return -EINVAL;
|
|
|
|
get_register_interruptible(ab, (u8)ab->debug_bank,
|
|
(u8)ab->debug_address, ®value);
|
|
if (err)
|
|
return -EINVAL;
|
|
|
|
return count;
|
|
}
|
|
|
|
static const struct file_operations ab3550_bank_fops = {
|
|
.open = ab3550_bank_open,
|
|
.write = ab3550_bank_write,
|
|
.read = seq_read,
|
|
.llseek = seq_lseek,
|
|
.release = single_release,
|
|
.owner = THIS_MODULE,
|
|
};
|
|
|
|
static const struct file_operations ab3550_address_fops = {
|
|
.open = ab3550_address_open,
|
|
.write = ab3550_address_write,
|
|
.read = seq_read,
|
|
.llseek = seq_lseek,
|
|
.release = single_release,
|
|
.owner = THIS_MODULE,
|
|
};
|
|
|
|
static const struct file_operations ab3550_val_fops = {
|
|
.open = ab3550_val_open,
|
|
.write = ab3550_val_write,
|
|
.read = seq_read,
|
|
.llseek = seq_lseek,
|
|
.release = single_release,
|
|
.owner = THIS_MODULE,
|
|
};
|
|
|
|
static struct dentry *ab3550_dir;
|
|
static struct dentry *ab3550_reg_file;
|
|
static struct dentry *ab3550_bank_file;
|
|
static struct dentry *ab3550_address_file;
|
|
static struct dentry *ab3550_val_file;
|
|
|
|
static inline void ab3550_setup_debugfs(struct ab3550 *ab)
|
|
{
|
|
ab->debug_bank = 0;
|
|
ab->debug_address = 0x00;
|
|
|
|
ab3550_dir = debugfs_create_dir(AB3550_NAME_STRING, NULL);
|
|
if (!ab3550_dir)
|
|
goto exit_no_debugfs;
|
|
|
|
ab3550_reg_file = debugfs_create_file("all-registers",
|
|
S_IRUGO, ab3550_dir, ab, &ab3550_registers_fops);
|
|
if (!ab3550_reg_file)
|
|
goto exit_destroy_dir;
|
|
|
|
ab3550_bank_file = debugfs_create_file("register-bank",
|
|
(S_IRUGO | S_IWUSR), ab3550_dir, ab, &ab3550_bank_fops);
|
|
if (!ab3550_bank_file)
|
|
goto exit_destroy_reg;
|
|
|
|
ab3550_address_file = debugfs_create_file("register-address",
|
|
(S_IRUGO | S_IWUSR), ab3550_dir, ab, &ab3550_address_fops);
|
|
if (!ab3550_address_file)
|
|
goto exit_destroy_bank;
|
|
|
|
ab3550_val_file = debugfs_create_file("register-value",
|
|
(S_IRUGO | S_IWUSR), ab3550_dir, ab, &ab3550_val_fops);
|
|
if (!ab3550_val_file)
|
|
goto exit_destroy_address;
|
|
|
|
return;
|
|
|
|
exit_destroy_address:
|
|
debugfs_remove(ab3550_address_file);
|
|
exit_destroy_bank:
|
|
debugfs_remove(ab3550_bank_file);
|
|
exit_destroy_reg:
|
|
debugfs_remove(ab3550_reg_file);
|
|
exit_destroy_dir:
|
|
debugfs_remove(ab3550_dir);
|
|
exit_no_debugfs:
|
|
dev_err(&ab->i2c_client[0]->dev, "failed to create debugfs entries.\n");
|
|
return;
|
|
}
|
|
|
|
static inline void ab3550_remove_debugfs(void)
|
|
{
|
|
debugfs_remove(ab3550_val_file);
|
|
debugfs_remove(ab3550_address_file);
|
|
debugfs_remove(ab3550_bank_file);
|
|
debugfs_remove(ab3550_reg_file);
|
|
debugfs_remove(ab3550_dir);
|
|
}
|
|
|
|
#else /* !CONFIG_DEBUG_FS */
|
|
static inline void ab3550_setup_debugfs(struct ab3550 *ab)
|
|
{
|
|
}
|
|
static inline void ab3550_remove_debugfs(void)
|
|
{
|
|
}
|
|
#endif
|
|
|
|
/*
|
|
* Basic set-up, datastructure creation/destruction and I2C interface.
|
|
* This sets up a default config in the AB3550 chip so that it
|
|
* will work as expected.
|
|
*/
|
|
static int __init ab3550_setup(struct ab3550 *ab)
|
|
{
|
|
int err = 0;
|
|
int i;
|
|
struct ab3550_platform_data *plf_data;
|
|
struct abx500_init_settings *settings;
|
|
|
|
plf_data = ab->i2c_client[0]->dev.platform_data;
|
|
settings = plf_data->init_settings;
|
|
|
|
for (i = 0; i < plf_data->init_settings_sz; i++) {
|
|
err = mask_and_set_register_interruptible(ab,
|
|
settings[i].bank,
|
|
settings[i].reg,
|
|
0xFF, settings[i].setting);
|
|
if (err)
|
|
goto exit_no_setup;
|
|
|
|
/* If event mask register update the event mask in ab3550 */
|
|
if ((settings[i].bank == 0) &&
|
|
(AB3550_IMR1 <= settings[i].reg) &&
|
|
(settings[i].reg <= AB3550_IMR5)) {
|
|
ab->event_mask[settings[i].reg - AB3550_IMR1] =
|
|
settings[i].setting;
|
|
}
|
|
}
|
|
exit_no_setup:
|
|
return err;
|
|
}
|
|
|
|
static void ab3550_mask_work(struct work_struct *work)
|
|
{
|
|
struct ab3550 *ab = container_of(work, struct ab3550, mask_work);
|
|
int i;
|
|
unsigned long flags;
|
|
u8 mask[AB3550_NUM_EVENT_REG];
|
|
|
|
spin_lock_irqsave(&ab->event_lock, flags);
|
|
for (i = 0; i < AB3550_NUM_EVENT_REG; i++)
|
|
mask[i] = ab->event_mask[i];
|
|
spin_unlock_irqrestore(&ab->event_lock, flags);
|
|
|
|
for (i = 0; i < AB3550_NUM_EVENT_REG; i++) {
|
|
int err;
|
|
|
|
err = mask_and_set_register_interruptible(ab, 0,
|
|
(AB3550_IMR1 + i), ~0, mask[i]);
|
|
if (err)
|
|
dev_err(&ab->i2c_client[0]->dev,
|
|
"ab3550_mask_work failed 0x%x,0x%x\n",
|
|
(AB3550_IMR1 + i), mask[i]);
|
|
}
|
|
}
|
|
|
|
static void ab3550_mask(struct irq_data *data)
|
|
{
|
|
unsigned long flags;
|
|
struct ab3550 *ab;
|
|
struct ab3550_platform_data *plf_data;
|
|
int irq;
|
|
|
|
ab = irq_data_get_irq_chip_data(data);
|
|
plf_data = ab->i2c_client[0]->dev.platform_data;
|
|
irq = data->irq - plf_data->irq.base;
|
|
|
|
spin_lock_irqsave(&ab->event_lock, flags);
|
|
ab->event_mask[irq / 8] |= BIT(irq % 8);
|
|
spin_unlock_irqrestore(&ab->event_lock, flags);
|
|
|
|
schedule_work(&ab->mask_work);
|
|
}
|
|
|
|
static void ab3550_unmask(struct irq_data *data)
|
|
{
|
|
unsigned long flags;
|
|
struct ab3550 *ab;
|
|
struct ab3550_platform_data *plf_data;
|
|
int irq;
|
|
|
|
ab = irq_data_get_irq_chip_data(data);
|
|
plf_data = ab->i2c_client[0]->dev.platform_data;
|
|
irq = data->irq - plf_data->irq.base;
|
|
|
|
spin_lock_irqsave(&ab->event_lock, flags);
|
|
ab->event_mask[irq / 8] &= ~BIT(irq % 8);
|
|
spin_unlock_irqrestore(&ab->event_lock, flags);
|
|
|
|
schedule_work(&ab->mask_work);
|
|
}
|
|
|
|
static void noop(struct irq_data *data)
|
|
{
|
|
}
|
|
|
|
static struct irq_chip ab3550_irq_chip = {
|
|
.name = "ab3550-core", /* Keep the same name as the request */
|
|
.irq_disable = ab3550_mask, /* No default to mask in chip.c */
|
|
.irq_ack = noop,
|
|
.irq_mask = ab3550_mask,
|
|
.irq_unmask = ab3550_unmask,
|
|
};
|
|
|
|
struct ab_family_id {
|
|
u8 id;
|
|
char *name;
|
|
};
|
|
|
|
static const struct ab_family_id ids[] __initdata = {
|
|
/* AB3550 */
|
|
{
|
|
.id = AB3550_P1A,
|
|
.name = "P1A"
|
|
},
|
|
/* Terminator */
|
|
{
|
|
.id = 0x00,
|
|
}
|
|
};
|
|
|
|
static int __init ab3550_probe(struct i2c_client *client,
|
|
const struct i2c_device_id *id)
|
|
{
|
|
struct ab3550 *ab;
|
|
struct ab3550_platform_data *ab3550_plf_data =
|
|
client->dev.platform_data;
|
|
int err;
|
|
int i;
|
|
int num_i2c_clients = 0;
|
|
|
|
ab = kzalloc(sizeof(struct ab3550), GFP_KERNEL);
|
|
if (!ab) {
|
|
dev_err(&client->dev,
|
|
"could not allocate " AB3550_NAME_STRING " device\n");
|
|
return -ENOMEM;
|
|
}
|
|
|
|
/* Initialize data structure */
|
|
mutex_init(&ab->access_mutex);
|
|
spin_lock_init(&ab->event_lock);
|
|
ab->i2c_client[0] = client;
|
|
|
|
i2c_set_clientdata(client, ab);
|
|
|
|
/* Read chip ID register */
|
|
err = get_register_interruptible(ab, 0, AB3550_CID_REG, &ab->chip_id);
|
|
if (err) {
|
|
dev_err(&client->dev, "could not communicate with the analog "
|
|
"baseband chip\n");
|
|
goto exit_no_detect;
|
|
}
|
|
|
|
for (i = 0; ids[i].id != 0x0; i++) {
|
|
if (ids[i].id == ab->chip_id) {
|
|
snprintf(&ab->chip_name[0], sizeof(ab->chip_name) - 1,
|
|
AB3550_ID_FORMAT_STRING, ids[i].name);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (ids[i].id == 0x0) {
|
|
dev_err(&client->dev, "unknown analog baseband chip id: 0x%x\n",
|
|
ab->chip_id);
|
|
dev_err(&client->dev, "driver not started!\n");
|
|
goto exit_no_detect;
|
|
}
|
|
|
|
dev_info(&client->dev, "detected AB chip: %s\n", &ab->chip_name[0]);
|
|
|
|
/* Attach other dummy I2C clients. */
|
|
while (++num_i2c_clients < AB3550_NUM_BANKS) {
|
|
ab->i2c_client[num_i2c_clients] =
|
|
i2c_new_dummy(client->adapter,
|
|
(client->addr + num_i2c_clients));
|
|
if (!ab->i2c_client[num_i2c_clients]) {
|
|
err = -ENOMEM;
|
|
goto exit_no_dummy_client;
|
|
}
|
|
strlcpy(ab->i2c_client[num_i2c_clients]->name, id->name,
|
|
sizeof(ab->i2c_client[num_i2c_clients]->name));
|
|
}
|
|
|
|
err = ab3550_setup(ab);
|
|
if (err)
|
|
goto exit_no_setup;
|
|
|
|
INIT_WORK(&ab->mask_work, ab3550_mask_work);
|
|
|
|
for (i = 0; i < ab3550_plf_data->irq.count; i++) {
|
|
unsigned int irq;
|
|
|
|
irq = ab3550_plf_data->irq.base + i;
|
|
irq_set_chip_data(irq, ab);
|
|
irq_set_chip_and_handler(irq, &ab3550_irq_chip,
|
|
handle_simple_irq);
|
|
irq_set_nested_thread(irq, 1);
|
|
#ifdef CONFIG_ARM
|
|
set_irq_flags(irq, IRQF_VALID);
|
|
#else
|
|
irq_set_noprobe(irq);
|
|
#endif
|
|
}
|
|
|
|
err = request_threaded_irq(client->irq, NULL, ab3550_irq_handler,
|
|
IRQF_ONESHOT, "ab3550-core", ab);
|
|
/* This real unpredictable IRQ is of course sampled for entropy */
|
|
rand_initialize_irq(client->irq);
|
|
|
|
if (err)
|
|
goto exit_no_irq;
|
|
|
|
err = abx500_register_ops(&client->dev, &ab3550_ops);
|
|
if (err)
|
|
goto exit_no_ops;
|
|
|
|
/* Set up and register the platform devices. */
|
|
for (i = 0; i < AB3550_NUM_DEVICES; i++) {
|
|
ab3550_devs[i].platform_data = ab3550_plf_data->dev_data[i];
|
|
ab3550_devs[i].pdata_size = ab3550_plf_data->dev_data_sz[i];
|
|
}
|
|
|
|
err = mfd_add_devices(&client->dev, 0, ab3550_devs,
|
|
ARRAY_SIZE(ab3550_devs), NULL,
|
|
ab3550_plf_data->irq.base);
|
|
|
|
ab3550_setup_debugfs(ab);
|
|
|
|
return 0;
|
|
|
|
exit_no_ops:
|
|
exit_no_irq:
|
|
exit_no_setup:
|
|
exit_no_dummy_client:
|
|
/* Unregister the dummy i2c clients. */
|
|
while (--num_i2c_clients)
|
|
i2c_unregister_device(ab->i2c_client[num_i2c_clients]);
|
|
exit_no_detect:
|
|
kfree(ab);
|
|
return err;
|
|
}
|
|
|
|
static int __exit ab3550_remove(struct i2c_client *client)
|
|
{
|
|
struct ab3550 *ab = i2c_get_clientdata(client);
|
|
int num_i2c_clients = AB3550_NUM_BANKS;
|
|
|
|
mfd_remove_devices(&client->dev);
|
|
ab3550_remove_debugfs();
|
|
|
|
while (--num_i2c_clients)
|
|
i2c_unregister_device(ab->i2c_client[num_i2c_clients]);
|
|
|
|
/*
|
|
* At this point, all subscribers should have unregistered
|
|
* their notifiers so deactivate IRQ
|
|
*/
|
|
free_irq(client->irq, ab);
|
|
kfree(ab);
|
|
return 0;
|
|
}
|
|
|
|
static const struct i2c_device_id ab3550_id[] = {
|
|
{AB3550_NAME_STRING, 0},
|
|
{}
|
|
};
|
|
MODULE_DEVICE_TABLE(i2c, ab3550_id);
|
|
|
|
static struct i2c_driver ab3550_driver = {
|
|
.driver = {
|
|
.name = AB3550_NAME_STRING,
|
|
.owner = THIS_MODULE,
|
|
},
|
|
.id_table = ab3550_id,
|
|
.probe = ab3550_probe,
|
|
.remove = __exit_p(ab3550_remove),
|
|
};
|
|
|
|
static int __init ab3550_i2c_init(void)
|
|
{
|
|
return i2c_add_driver(&ab3550_driver);
|
|
}
|
|
|
|
static void __exit ab3550_i2c_exit(void)
|
|
{
|
|
i2c_del_driver(&ab3550_driver);
|
|
}
|
|
|
|
subsys_initcall(ab3550_i2c_init);
|
|
module_exit(ab3550_i2c_exit);
|
|
|
|
MODULE_AUTHOR("Mattias Wallin <mattias.wallin@stericsson.com>");
|
|
MODULE_DESCRIPTION("AB3550 core driver");
|
|
MODULE_LICENSE("GPL");
|