blob: d4159ee92d3f6adc4a87dbefbb5090edb855f2ca [file] [log] [blame]
/*
* Device driver for monitoring ambient light intensity (lux)
* within the TAOS tsl258x family of devices (tsl2580, tsl2581).
*
* Copyright (c) 2011, TAOS Corporation.
*
* 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; either version 2 of the License, or
* (at your option) any later version.
*
* 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.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include <linux/kernel.h>
#include <linux/i2c.h>
#include <linux/errno.h>
#include <linux/delay.h>
#include <linux/string.h>
#include <linux/mutex.h>
#include <linux/unistd.h>
#include <linux/slab.h>
#include <linux/module.h>
#include <linux/input.h>
#include <linux/i2c.h>
#include <linux/gpio.h>
#include <linux/time.h>
#include <linux/jiffies.h>
#include <linux/posix-timers.h>
#include <linux/pm_runtime.h>
#include <linux/tsl258x_als.h>
#include <asm/intel-mid.h>
#define TSL258X_NAME "tsl2584"
#define ID_UNKNOWN 0
#define ID_TSL2580 1 /* the TSL2580 is SMBus version of TSL2581 */
#define ID_TSL2581 2
#define ID_TSL2583 3
#define ID_TSL2584TSV 4
#define TSL258X_CHANNEL0 0x00
#define TSL258X_CHANNEL1 0x01
#define TSL258X_MAX_DEVICE_REGS 32
/* Triton register offsets */
#define TSL258X_REG_MAX 8
/* Device Registers and Masks */
#define TSL258X_CNTRL 0x00
#define TSL258X_ALS_TIME 0X01
#define TSL258X_INTERRUPT 0x02
#define TSL258X_GAIN 0x07
#define TSL258X_REVID 0x11
#define TSL258X_CHIPID 0x12
#define TSL258X_ALS_CHAN0LO 0x14
#define TSL258X_ALS_CHAN0HI 0x15
#define TSL258X_ALS_CHAN1LO 0x16
#define TSL258X_ALS_CHAN1HI 0x17
#define TSL258X_TMR_LO 0x18
#define TSL258X_TMR_HI 0x19
#define TSL258X_ID2 0x1e
/* tsl258x cmd reg masks */
#define TSL258X_CMD_REG 0x80
#define TSL258X_CMD_SPL_FN 0x60
#define TSL258X_CMD_AUTO_INC 0x20
#define TSL258X_CMD_ALS_INT_CLR 0x01
/* tsl258x cntrl reg masks */
#define TSL258X_CNTL_ADC_ENBL 0x02
#define TSL258X_CNTL_PWR_ON 0x01
#define TSL258X_CNTL_PWR_OFF 0x00
/* tsl258x status reg masks */
#define TSL258X_STA_ADC_VALID 0x01
#define TSL258X_STA_ADC_INTR 0x10
/* Lux calculation constants */
#define TSL258X_LUX_CALC_OVER_FLOW 65535
/* constants for lux calculations */
#define MPOW 13
#define MFACT (1 << MPOW)
#define ALS_TIME_MIN 50
#define ALS_TIME_MAX 650
#define ALS_TIME_TO_COUNT(ms) ((((ms) * 100 + 135) / 270) ? (((ms) * 100 + 135) / 270) : 1)
#define ALS_COUNT_TO_TIME(count) (((count) * 27 + 5) / 10)
#define GAIN_RATIO 16
/*******************************************************************
start TSL2584TSV lux equation defines
The lux equation is of the form:
coff_1 * ((coff_ch0 * ch0) - (coff_ch1 * ch1)) / (atime * again)
which can be rearranged as:
((coff_1 * coff_ch0 * ch0) - (coff_1 * coff_ch1 * ch1)) / (atime * again)
In order to maintain precision using integers, we multiply numerator and denominator by a power of 2
(((coff_1 * coff_ch0 * (2 ^ MPOW)) * ch0) - ((coff_1 * coff_ch1 * (2 ^ MPOW)) * ch1)) / (atime * again * (2 ^ MPOW))
which is the same as:
(((coff_1 * coff_ch0 * (2 ^ MPOW)) * ch0) - ((coff_1 * coff_ch1 * (2 ^ MPOW)) * ch1)) / ((atime * again) << MPOW))
since atime * again is an integer
*******************************************************************/
/* No glass Coefficients */
#define COFF_1_NO_GLASS 105
#define COFF_CH0_NO_GLASS (u32)(1 * MFACT)
#define COFF_CH1_NO_GLASS (u32)(1.13 * MFACT)
#define CH0_COFF_NO_GLASS (u32)((COFF_1_NO_GLASS * COFF_CH0_NO_GLASS))
#define CH1_COFF_NO_GLASS (u32)((COFF_1_NO_GLASS * COFF_CH1_NO_GLASS))
/* Add new glass coefficients here */
/* set the coefficients the TSL2584TSV equation will use */
#define TSL2584TSV_CH0_COFF CH0_COFF_NO_GLASS
#define TSL2584TSV_CH1_COFF CH1_COFF_NO_GLASS
/* end TSL2584TSV lux equation defines */
/******************************************************************
start TSL2584TSV lux equation defines on Marvin
The lux equation is of the form:
Lux1 = 1000*((Ch0-(2.16*Ch1))/(Atime*Again))
Lux2 = 1000*((0.95*Ch0)-(1.11*Ch1)/(Atime*Again))
Lux = MAX(Lux1,Lux2)
in other form:
lux1 = (1000*ch0 - 2160*ch1) / (Atime*Again)
lux2 = (950*ch0 - 1110*ch1) / (Atime*Again)
Lux = MAX(Lux1,Lux2)
******************************************************************/
/* set the coefficients the TSL2584TSV equation will use on Marvin */
#define TSL2584_CH0_COFF0 52300
#define TSL2584_CH1_COFF0 60200
#define TSL2584TSV_CH0_COFF0 1000
#define TSL2584TSV_CH1_COFF0 2160
#define TSL2584TSV_CH0_COFF1 950
#define TSL2584TSV_CH1_COFF1 1110
#define FORMULA_NUM 1
/* end TSL2584TSV lux equation defines on Marvin */
/* For als_status */
enum {
TSL258X_STATUS_UNKNOWN = 0, /* Initial state */
TSL258X_STATUS_ENABLED = 1, /* Enabled */
TSL258X_STATUS_SUSPENDED = 2, /* Disabled */
TSL258X_STATUS_POWERED_OFF = 3, /* Powered off */
};
struct taos_settings {
int als_odr;
int als_time;
int als_gain_idex;
int als_gain_trim;
int als_cal_target;
};
struct tsl258x_chip {
struct mutex als_mutex;
struct i2c_client *client;
struct input_dev *input;
struct timer_list timer;
struct work_struct data_work;
struct tsl258x_platform_data *pdata;
struct taos_settings taos_settings;
u16 lux;
int als_time_scale;
int als_saturation;
int als_status;
int id;
};
struct taos_lux {
unsigned int ratio;
unsigned int ch0;
unsigned int ch1;
};
/* This structure is intentionally large to accommodate updates via sysfs. */
/* Sized to 11 = max 10 segments + 1 termination segment */
/* Assumption is is one and only one type of glass used */
static struct taos_lux taos_device_lux[11];
/* tsl2581, tsl2583 */
static struct taos_lux taos_device_lux_tsl258x[] = {
{ 9830, 8520, 15729 },
{ 12452, 10807, 23344 },
{ 14746, 6383, 11705 },
{ 17695, 4063, 6554 }
};
/* tsl2585tsv */
static struct taos_lux taos_device_lux_tsl2584tsv[] = {
{ 0, CH0_COFF_NO_GLASS, CH1_COFF_NO_GLASS }
};
struct gainadj {
s16 ch0;
s16 ch1;
};
/* Index = (0 - 3) Used to validate the gain selection index */
static const struct gainadj gainadj[] = {
{ 1, 1 },
{ 8, 8 },
{ 16, 16 },
{ 107, 115 }
};
static struct tsl2584_als_gain_tbl {
int gain_idex;
int gain_val;
} tsl2584_als_gain_tbl[] = {
[0] = {
.gain_idex = TSL2584_ALS_GAIN1,
.gain_val = 1,
},
[1] = {
.gain_idex = TSL2584_ALS_GAIN8,
.gain_val = 8,
},
[2] = {
.gain_idex = TSL2584_ALS_GAIN16,
.gain_val = 16,
},
[3] = {
.gain_idex = TSL2584_ALS_GAIN111,
.gain_val = 111,
},
};
static struct workqueue_struct *tsl258x_wq;
static char *tsl2583x_get_name(struct tsl258x_chip *chip);
/*
* Provides initial operational parameter defaults.
* These defaults may be changed through the device's sysfs files.
*/
static void taos_defaults(struct tsl258x_chip *chip)
{
chip->taos_settings.als_odr = chip->pdata->als_def_odr;
/* assume clear glass as default */
chip->taos_settings.als_gain_trim = chip->pdata->als_def_gain_trim;
/* default gain trim to account for aperture effects */
chip->taos_settings.als_cal_target = chip->pdata->als_def_cal_target;
/* populate the default lux table */
if (chip->id == ID_TSL2584TSV) {
memcpy(&taos_device_lux[0],
&taos_device_lux_tsl2584tsv[0],
sizeof(taos_device_lux_tsl2584tsv));
} else {
memcpy(&taos_device_lux[0],
&taos_device_lux_tsl258x[0],
sizeof(taos_device_lux_tsl258x));
}
/* Known external ALS reading used for calibration */
}
/* i2c smbus read access */
static int taos_i2c_smbus_read(struct i2c_client *client)
{
int ret;
pm_runtime_get_sync(&client->dev);
ret = i2c_smbus_read_byte(client);
pm_runtime_put(&client->dev);
return ret;
}
/* i2c smbus write access */
static int taos_i2c_smbus_write(struct i2c_client *client, u8 command)
{
int ret;
pm_runtime_get_sync(&client->dev);
ret = i2c_smbus_write_byte(client, command);
pm_runtime_put(&client->dev);
return ret;
}
static int taos_i2c_smbus_write_data(struct i2c_client *client, u8 command, u8 value)
{
int ret;
pm_runtime_get_sync(&client->dev);
ret = i2c_smbus_write_byte_data(client, command, value);
pm_runtime_put(&client->dev);
return ret;
}
/*
* Read a number of bytes starting at register (reg) location.
* Return 0, or taos_i2c_smbus_write ERROR code.
*/
static int
taos_i2c_read(struct i2c_client *client, u8 reg, u8 *val, unsigned int len)
{
int i, ret;
for (i = 0; i < len; i++) {
/* select register to write */
ret = taos_i2c_smbus_write(client, (TSL258X_CMD_REG | reg));
if (ret < 0) {
dev_err(&client->dev, "taos_i2c_read failed to write"
" register %x\n", reg);
return ret;
}
/* read the data */
*val = taos_i2c_smbus_read(client);
val++;
reg++;
}
return 0;
}
static int taos_set_power(struct tsl258x_chip *chip, int on)
{
u8 cntrl = 0;
int ret = 0;
if (!on) {
dev_info(&chip->client->dev, "powering down sensor\n");
cntrl = TSL258X_CNTL_PWR_OFF;
} else {
dev_info(&chip->client->dev, "powering up sensor\n");
cntrl = TSL258X_CNTL_PWR_ON;
}
ret = taos_i2c_smbus_write_data(chip->client,
TSL258X_CMD_REG | TSL258X_CNTRL, cntrl);
if (!ret && on) {
/* Transition from powered off to disabled */
chip->als_status = TSL258X_STATUS_SUSPENDED;
mdelay(3);
} else if (!ret && !on) {
/* Transition from enabled/disabled to powered off */
chip->als_status = TSL258X_STATUS_POWERED_OFF;
} else {
dev_err(&chip->client->dev, "failed to power %s the sensor\n",
(on == 1) ? "up" : "down");
}
return ret;
}
static int taos_set_enable(struct tsl258x_chip *chip, int en)
{
u8 cntrl = 0;
int ret = 0;
if (!en) {
cntrl = TSL258X_CNTL_PWR_ON;
dev_info(&chip->client->dev, "disabling sensor\n");
} else {
cntrl = TSL258X_CNTL_PWR_ON | TSL258X_CNTL_ADC_ENBL;
dev_info(&chip->client->dev, "enabling sensor\n");
dev_dbg(&chip->client->dev, "%s: integration time als_time=%dms\n",
__func__, chip->taos_settings.als_time);
dev_dbg(&chip->client->dev, "%s: polling period als_odr=%dms\n",
__func__, chip->taos_settings.als_odr);
}
ret = taos_i2c_smbus_write_data(chip->client,
TSL258X_CMD_REG | TSL258X_CNTRL, cntrl);
if (!ret && en)
chip->als_status = TSL258X_STATUS_ENABLED;
else if (!ret && !en)
chip->als_status = TSL258X_STATUS_SUSPENDED;
else
dev_err(&chip->client->dev, "failed to %s the sensor\n",
(en == 1) ? "enable" : "disable");
return ret;
}
static int taos_set_als_time(struct tsl258x_chip *chip, int ms)
{
int ret = 0;
int als_count = 0;
int als_time = ms;
if (chip->taos_settings.als_time == ms)
return 0;
if (als_time > ALS_TIME_MAX)
als_time = ALS_TIME_MAX;
else if (als_time < ALS_TIME_MIN)
als_time = ALS_TIME_MIN;
/* determine als integration regster, at least one cycle */
als_count = ALS_TIME_TO_COUNT(als_time);
/* convert back to time (encompasses overrides) */
als_time = ALS_COUNT_TO_TIME(als_count);
ret = taos_i2c_smbus_write_data(chip->client,
TSL258X_CMD_REG | TSL258X_ALS_TIME,
(256 - als_count));
if (ret < 0)
return ret;
chip->taos_settings.als_time = als_time;
/* set chip struct re scaling and saturation */
chip->als_saturation = als_count * 922; /* 90% of full scale */
chip->als_time_scale = (als_time + 25) / 50;
return ret;
}
static int taos_set_gain(struct tsl258x_chip *chip, int gain)
{
int i;
int ret = 0;
for (i = 0; i < TSL2584_ALS_GAIN_NUM; i++) {
if (gain == tsl2584_als_gain_tbl[i].gain_val)
break;
}
if (i >= TSL2584_ALS_GAIN_NUM)
return -EINVAL;
ret = taos_i2c_smbus_write_data(chip->client,
TSL258X_CMD_REG | TSL258X_GAIN, tsl2584_als_gain_tbl[i].gain_idex);
if (ret < 0)
return ret;
chip->taos_settings.als_gain_idex = tsl2584_als_gain_tbl[i].gain_idex;
return ret;
}
static int taos_channel_data_valid(struct tsl258x_chip *chip)
{
u8 cntrl = 0;
int ret = 0;
ret = taos_i2c_read(chip->client, TSL258X_CNTRL, &cntrl, 1);
if (ret < 0) {
dev_err(&chip->client->dev, "failed to read ctrnl\n");
return ret;
}
if (!(cntrl & TSL258X_STA_ADC_INTR)) {
dev_err(&chip->client->dev, "channel data not new\n");
return -ENODATA;
}
if (!(cntrl & TSL258X_STA_ADC_VALID)) {
dev_err(&chip->client->dev, "channel data not valid\n");
return -ENODATA;
}
return ret;
}
static int taos_read_channel_data(struct tsl258x_chip *chip, int channel, u16 *val)
{
int ret = 0;
if (channel == TSL258X_CHANNEL0)
ret = i2c_smbus_read_word_data(chip->client,
TSL258X_CMD_REG | TSL258X_CMD_AUTO_INC | TSL258X_ALS_CHAN0LO);
else
ret = i2c_smbus_read_word_data(chip->client,
TSL258X_CMD_REG | TSL258X_CMD_AUTO_INC | TSL258X_ALS_CHAN1LO);
if (ret >= 0) {
*val = ret;
ret = 0;
}
return ret;
}
static int taos_chip_clear_interrupt(struct tsl258x_chip *chip)
{
return taos_i2c_smbus_write(chip->client,
TSL258X_CMD_REG | TSL258X_CMD_SPL_FN | TSL258X_CMD_ALS_INT_CLR);
}
static int taos_init_configure(struct tsl258x_chip *chip,
struct tsl258x_platform_data *pdata)
{
int ret = 0;
ret = taos_set_power(chip, true);
if (ret)
return ret;
ret = taos_set_als_time(chip, pdata->als_def_als_time);
if (ret)
return ret;
ret = taos_set_gain(chip, pdata->als_def_gain);
return ret;
}
/*
* Reads and calculates current lux value.
* The raw ch0 and ch1 values of the ambient light sensed in the last
* integration cycle are read from the device.
* Time scale factor array values are adjusted based on the integration time.
* The raw values are multiplied by a scale factor, and device gain is obtained
* using gain index. Limit checks are done next, then the ratio of a multiple
* of ch1 value, to the ch0 value, is calculated. The array taos_device_lux[]
* declared above is then scanned to find the first ratio value that is just
* above the ratio we just calculated. The ch0 and ch1 multiplier constants in
* the array are then used along with the time scale factor array values, to
* calculate the lux.
*/
static int taos_get_lux(struct tsl258x_chip *chip)
{
u16 ch0, ch1; /* separated ch0/ch1 data from device */
int lux; /* raw lux calculated from device data */
u32 ratio;
struct taos_lux *p;
int ret;
u32 ch0lux = 0;
u32 ch1lux = 0;
int gain;
int lux1;
int lux2;
if (chip->als_status != TSL258X_STATUS_ENABLED) {
/* device is not enabled */
dev_err(&chip->client->dev, "taos_get_lux device is not enabled\n");
return -EBUSY;
}
ret = taos_channel_data_valid(chip);
if (ret < 0)
return ret;
ret = taos_read_channel_data(chip, TSL258X_CHANNEL0, &ch0);
if (ret < 0) {
dev_err(&chip->client->dev, "taos_get_lux failed to read ch0\n");
return ret;
}
ret = taos_read_channel_data(chip, TSL258X_CHANNEL1, &ch1);
if (ret < 0) {
dev_err(&chip->client->dev, "taos_get_lux failed to read ch1\n");
return ret;
}
/* clear status, really interrupt status (interrupts are off), but we use the bit anyway */
ret = taos_chip_clear_interrupt(chip);
if (ret < 0) {
dev_err(&chip->client->dev, "taos_chip_clear_interrupt fail\n");
return ret;
}
if ((ch0 >= chip->als_saturation) || (ch1 >= chip->als_saturation)) {
chip->lux = TSL258X_LUX_CALC_OVER_FLOW;
lux = chip->lux;
return lux;
}
if (ch0 == 0) {
/* have no data, so return LAST VALUE */
dev_info(&chip->client->dev, "ch0 has no data\n");
lux = chip->lux;
chip->lux = 0;
return lux;
}
if (chip->id == ID_TSL2584TSV) {
gain = chip->taos_settings.als_time *
tsl2584_als_gain_tbl[chip->taos_settings.als_gain_idex].gain_val;
if (FORMULA_NUM == chip->pdata->als_def_product_formula_num) {
lux1 = (TSL2584TSV_CH0_COFF0 * ch0 - TSL2584TSV_CH1_COFF0 * ch1) / gain;
lux2 = (TSL2584TSV_CH0_COFF1 * ch0 - TSL2584TSV_CH1_COFF1 * ch1) / gain;
if ((lux1 < 0) && (lux2 < 0))
return -ERANGE;
lux = (lux1 >= lux2) ? lux1 : lux2;
lux /= GAIN_RATIO;
} else {
lux = (TSL2584_CH0_COFF0 * ch0 - TSL2584_CH1_COFF0 * ch1) / gain;
if (lux < 0)
return -ERANGE;
}
} else {
/* calculate ratio */
ratio = (ch1 << 15) / ch0;
/* convert to unscaled lux using the pointer to the table */
for (p = (struct taos_lux *) taos_device_lux;
p->ratio != 0 && p->ratio < ratio; p++)
;
if (p->ratio == 0) {
lux = 0;
} else {
ch0lux = ((ch0 * p->ch0) +
(gainadj[chip->taos_settings.als_gain_idex].ch0 >> 1))
/ gainadj[chip->taos_settings.als_gain_idex].ch0;
ch1lux = ((ch1 * p->ch1) +
(gainadj[chip->taos_settings.als_gain_idex].ch1 >> 1))
/ gainadj[chip->taos_settings.als_gain_idex].ch1;
lux = ch0lux - ch1lux;
}
/* note: lux is 31 bit max at this point */
if (ch1lux > ch0lux) {
dev_info(&chip->client->dev, "No Data - Return last value\n");
lux = chip->lux;
chip->lux = 0;
return lux;
}
/* adjust for active time scale */
if (chip->als_time_scale == 0)
lux = 0;
else
lux = (lux + (chip->als_time_scale >> 1)) / chip->als_time_scale;
/* adjust for active gain scale */
lux >>= 13; /* tables have factor of 8192 builtin for accuracy */
lux = (lux * chip->taos_settings.als_gain_trim + 500) / 1000;
}
if (lux > TSL258X_LUX_CALC_OVER_FLOW) /* check for overflow */
lux = TSL258X_LUX_CALC_OVER_FLOW;
/* Update the structure with the latest VALID lux. */
chip->lux = lux;
return lux;
}
/*
* Obtain single reading and calculate the als_gain_trim (later used
* to derive actual lux).
* Return updated gain_trim value.
*/
static int taos_als_calibrate(struct tsl258x_chip *chip)
{
u8 reg_val;
unsigned int gain_trim_val;
int ret;
int lux_val;
ret = taos_i2c_smbus_write(chip->client,
(TSL258X_CMD_REG | TSL258X_CNTRL));
if (ret < 0) {
dev_err(&chip->client->dev,
"taos_als_calibrate failed to reach the CNTRL register, ret=%d\n",
ret);
return ret;
}
reg_val = taos_i2c_smbus_read(chip->client);
if ((reg_val & (TSL258X_CNTL_ADC_ENBL | TSL258X_CNTL_PWR_ON))
!= (TSL258X_CNTL_ADC_ENBL | TSL258X_CNTL_PWR_ON)) {
dev_err(&chip->client->dev,
"taos_als_calibrate failed: device not enabled\n");
return -1;
}
ret = taos_i2c_smbus_write(chip->client,
(TSL258X_CMD_REG | TSL258X_CNTRL));
if (ret < 0) {
dev_err(&chip->client->dev,
"taos_als_calibrate failed to reach the STATUS register, ret=%d\n",
ret);
return ret;
}
reg_val = taos_i2c_smbus_read(chip->client);
if ((reg_val & TSL258X_STA_ADC_VALID) != TSL258X_STA_ADC_VALID) {
dev_err(&chip->client->dev,
"taos_als_calibrate failed: STATUS - ADC not valid.\n");
return -ENODATA;
}
lux_val = taos_get_lux(chip);
if (lux_val < 0) {
dev_err(&chip->client->dev, "taos_als_calibrate failed to get lux\n");
return lux_val;
}
gain_trim_val = (unsigned int) (((chip->taos_settings.als_cal_target)
* chip->taos_settings.als_gain_trim) / lux_val);
if ((gain_trim_val < 250) || (gain_trim_val > 4000)) {
dev_err(&chip->client->dev,
"taos_als_calibrate failed: trim_val of %d is out of range\n",
gain_trim_val);
return -ENODATA;
}
chip->taos_settings.als_gain_trim = (int) gain_trim_val;
return (int) gain_trim_val;
}
static void taos_workqueue_handler(struct work_struct *work)
{
int ret = 0;
struct tsl258x_chip *chip =
container_of(work, struct tsl258x_chip, data_work);
mutex_lock(&chip->als_mutex);
if (chip->als_status != TSL258X_STATUS_ENABLED) {
mutex_unlock(&chip->als_mutex);
return;
}
ret = taos_get_lux(chip);
if (ret > 0) {
/* report the resultant_lux level */
input_event(chip->input, EV_MSC, MSC_RAW, ret);
/* Sync it up */
input_sync(chip->input);
}
mod_timer(&chip->timer,
jiffies + msecs_to_jiffies(chip->taos_settings.als_odr));
mutex_unlock(&chip->als_mutex);
}
static void taos_timer_handler(unsigned long private)
{
struct tsl258x_chip *chip = (struct tsl258x_chip *)private;
if (chip->als_status == TSL258X_STATUS_ENABLED)
queue_work(tsl258x_wq, &chip->data_work);
}
/* Sysfs Interface Functions */
static ssize_t taos_enable_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct i2c_client *client = to_i2c_client(dev);
struct tsl258x_chip *chip = i2c_get_clientdata(client);
int enable = 0;
if (chip->als_status == TSL258X_STATUS_ENABLED)
enable = 1;
else
enable = 0;
return sprintf(buf, "%d\n", enable);
}
static ssize_t taos_enable_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t len)
{
struct i2c_client *client = to_i2c_client(dev);
struct tsl258x_chip *chip = i2c_get_clientdata(client);
unsigned int value;
int err;
if (kstrtouint(buf, 0, &value))
return -EINVAL;
if ((value != 0) && (value != 1))
return -EINVAL;
mutex_lock(&chip->als_mutex);
if (value == 1) {
/* Enable light sensor */
if (chip->als_status != TSL258X_STATUS_ENABLED) {
err = taos_set_enable(chip, true);
if (err) {
dev_err(&client->dev, "taos_set_enable true failed\n");
len = err;
goto enable_als_err;
} else {
mod_timer(&chip->timer,
jiffies + msecs_to_jiffies(chip->taos_settings.als_odr));
}
}
} else {
/* Disable light sensor */
err = taos_set_enable(chip, false);
if (err) {
dev_err(&client->dev, "taos_set_enable false failed\n");
len = err;
goto enable_als_err;
}
}
enable_als_err:
mutex_unlock(&chip->als_mutex);
return len;
}
static ssize_t taos_als_delay_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct tsl258x_chip *chip = i2c_get_clientdata(to_i2c_client(dev));
return sprintf(buf, "%d\n", chip->taos_settings.als_odr);
}
static ssize_t taos_als_delay_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t len)
{
struct i2c_client *client = to_i2c_client(dev);
struct tsl258x_chip *chip = i2c_get_clientdata(client);
unsigned int value;
if (kstrtouint(buf, 0, &value))
return -EINVAL;
if (!value)
return -EINVAL;
if (value < chip->taos_settings.als_time)
dev_warn(&client->dev,
"warning: polling period als_odr=%dms shorter than integration time als_time=%dms\n",
value, chip->taos_settings.als_time);
mutex_lock(&chip->als_mutex);
chip->taos_settings.als_odr = value;
mutex_unlock(&chip->als_mutex);
return len;
}
static ssize_t taos_gain_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct i2c_client *client = to_i2c_client(dev);
struct tsl258x_chip *chip = i2c_get_clientdata(client);
int value;
value = tsl2584_als_gain_tbl[chip->taos_settings.als_gain_idex].gain_val;
return sprintf(buf, "%d\n", value);
}
static ssize_t taos_gain_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t len)
{
struct i2c_client *client = to_i2c_client(dev);
struct tsl258x_chip *chip = i2c_get_clientdata(client);
int i, err, value;
if (kstrtoint(buf, 0, &value))
return -EINVAL;
for (i = 0; i < TSL2584_ALS_GAIN_NUM; i++) {
if (tsl2584_als_gain_tbl[i].gain_val == value)
break;
}
if (i >= TSL2584_ALS_GAIN_NUM)
return -EINVAL;
mutex_lock(&chip->als_mutex);
err = taos_set_gain(chip, value);
mutex_unlock(&chip->als_mutex);
if (err)
len = err;
return len;
}
static ssize_t taos_gain_available_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
return sprintf(buf, "%s\n", "1 8 16 111");
}
static ssize_t taos_als_time_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct tsl258x_chip *chip = i2c_get_clientdata(to_i2c_client(dev));
return sprintf(buf, "%d\n", chip->taos_settings.als_time);
}
static ssize_t taos_als_time_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t len)
{
struct i2c_client *client = to_i2c_client(dev);
struct tsl258x_chip *chip = i2c_get_clientdata(client);
unsigned int value;
int err;
if (kstrtouint(buf, 0, &value))
return -EINVAL;
if (!value)
return -EINVAL;
if (value > chip->taos_settings.als_odr)
dev_warn(&client->dev,
"warning: polling period als_odr=%dms shorter than integration time als_time=%dms\n",
chip->taos_settings.als_odr, value);
mutex_lock(&chip->als_mutex);
err = taos_set_als_time(chip, value);
mutex_unlock(&chip->als_mutex);
if (err)
len = err;
return len;
}
static ssize_t taos_als_time_available_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
return sprintf(buf, "%s\n",
"50 100 150 200 250 300 350 400 450 500 550 600 650");
}
static ssize_t taos_als_trim_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct tsl258x_chip *chip = i2c_get_clientdata(to_i2c_client(dev));
return sprintf(buf, "%d\n", chip->taos_settings.als_gain_trim);
}
static ssize_t taos_als_trim_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t len)
{
struct tsl258x_chip *chip = i2c_get_clientdata(to_i2c_client(dev));
unsigned long value;
if (kstrtoul(buf, 0, &value))
return -EINVAL;
if (value)
chip->taos_settings.als_gain_trim = value;
return len;
}
static ssize_t taos_als_cal_target_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct tsl258x_chip *chip = i2c_get_clientdata(to_i2c_client(dev));
return sprintf(buf, "%d\n", chip->taos_settings.als_cal_target);
}
static ssize_t taos_als_cal_target_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t len)
{
struct tsl258x_chip *chip = i2c_get_clientdata(to_i2c_client(dev));
unsigned long value;
if (kstrtoul(buf, 0, &value))
return -EINVAL;
if (value)
chip->taos_settings.als_cal_target = value;
return len;
}
static ssize_t taos_lux_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
int ret;
struct tsl258x_chip *chip = i2c_get_clientdata(to_i2c_client(dev));
mutex_lock(&chip->als_mutex);
ret = taos_get_lux(chip);
mutex_unlock(&chip->als_mutex);
if (ret < 0)
return ret;
return sprintf(buf, "%d\n", ret);
}
static ssize_t taos_do_calibrate(struct device *dev,
struct device_attribute *attr, const char *buf, size_t len)
{
struct tsl258x_chip *chip = i2c_get_clientdata(to_i2c_client(dev));
unsigned long value;
if (kstrtoul(buf, 0, &value))
return -EINVAL;
if (value == 1) {
mutex_lock(&chip->als_mutex);
taos_als_calibrate(chip);
mutex_unlock(&chip->als_mutex);
}
return len;
}
static ssize_t taos_luxtable_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
int i;
int offset = 0;
for (i = 0; i < ARRAY_SIZE(taos_device_lux); i++) {
offset += sprintf(buf + offset, "%d,%d,%d,",
taos_device_lux[i].ratio,
taos_device_lux[i].ch0,
taos_device_lux[i].ch1);
if (taos_device_lux[i].ratio == 0) {
/* We just printed the first "0" entry.
* Now get rid of the extra "," and break. */
offset--;
break;
}
}
offset += sprintf(buf + offset, "\n");
return offset;
}
static ssize_t taos_luxtable_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t len)
{
struct tsl258x_chip *chip = i2c_get_clientdata(to_i2c_client(dev));
int value[ARRAY_SIZE(taos_device_lux)*3 + 1];
int n;
get_options(buf, ARRAY_SIZE(value), value);
/* We now have an array of ints starting at value[1], and
* enumerated by value[0].
* We expect each group of three ints is one table entry,
* and the last table entry is all 0.
*/
n = value[0];
if (chip->id == ID_TSL2584TSV) {
if (n != 3) {
dev_info(dev, "LUX TABLE INPUT ERROR "
"TSL2584TSV input as 0,COFF_CH0,COFF_CH1\n");
return -EINVAL;
}
if (value[(n - 2)] != 0) {
dev_info(dev, "LUX TABLE INPUT ERROR, "
"TSL2584TSV ratio must be 0, not %d\n",
value[(n - 2)]);
return -EINVAL;
}
} else {
if ((n % 3) || n < 6 || n > ((ARRAY_SIZE(taos_device_lux) - 1) * 3)) {
dev_info(dev, "LUX TABLE INPUT ERROR 1 Value[0]=%d\n", n);
return -EINVAL;
}
if ((value[(n - 2)] | value[(n - 1)] | value[n]) != 0) {
dev_info(dev, "LUX TABLE INPUT ERROR 2 Value[0]=%d\n", n);
return -EINVAL;
}
}
if (chip->als_status == TSL258X_STATUS_ENABLED)
taos_set_enable(chip, false);
/* Zero out the table */
memset(taos_device_lux, 0, sizeof(taos_device_lux));
memcpy(taos_device_lux, &value[1], (value[0] * 4));
taos_set_enable(chip, true);
return len;
}
static DEVICE_ATTR(als_enable, S_IRUGO | S_IWUSR,
taos_enable_show, taos_enable_store);
static DEVICE_ATTR(als_delay, S_IRUGO | S_IWUSR,
taos_als_delay_show, taos_als_delay_store);
static DEVICE_ATTR(illuminance0_calibscale, S_IRUGO | S_IWUSR,
taos_gain_show, taos_gain_store);
static DEVICE_ATTR(illuminance0_calibscale_available, S_IRUGO,
taos_gain_available_show, NULL);
static DEVICE_ATTR(illuminance0_integration_time, S_IRUGO | S_IWUSR,
taos_als_time_show, taos_als_time_store);
static DEVICE_ATTR(illuminance0_integration_time_available, S_IRUGO,
taos_als_time_available_show, NULL);
static DEVICE_ATTR(illuminance0_calibbias, S_IRUGO | S_IWUSR,
taos_als_trim_show, taos_als_trim_store);
static DEVICE_ATTR(illuminance0_input_target, S_IRUGO | S_IWUSR,
taos_als_cal_target_show, taos_als_cal_target_store);
static DEVICE_ATTR(illuminance0_input, S_IRUGO, taos_lux_show, NULL);
static DEVICE_ATTR(illuminance0_calibrate, S_IWUSR, NULL, taos_do_calibrate);
static DEVICE_ATTR(illuminance0_lux_table, S_IRUGO | S_IWUSR,
taos_luxtable_show, taos_luxtable_store);
static struct attribute *sysfs_attrs_ctrl[] = {
&dev_attr_als_enable.attr,
&dev_attr_als_delay.attr,
&dev_attr_illuminance0_calibscale.attr, /* Gain */
&dev_attr_illuminance0_calibscale_available.attr,
&dev_attr_illuminance0_integration_time.attr, /* I time*/
&dev_attr_illuminance0_integration_time_available.attr,
&dev_attr_illuminance0_calibbias.attr, /* trim */
&dev_attr_illuminance0_input_target.attr,
&dev_attr_illuminance0_input.attr,
&dev_attr_illuminance0_calibrate.attr,
&dev_attr_illuminance0_lux_table.attr,
NULL
};
static struct attribute_group tsl258x_attribute_group = {
.attrs = sysfs_attrs_ctrl,
};
/* Use the default register values to identify the actual chip */
static int taos_tsl258x_chip_id(unsigned char *bufp)
{
int id;
if ((bufp[TSL258X_ID2] & 0x80) == 0x80)
id = ID_TSL2584TSV;
else if ((bufp[TSL258X_ID2] & 0x30) == 0x30)
id = ID_TSL2583;
else if ((bufp[TSL258X_ID2] & 0x30) == 0)
id = ID_TSL2581;
else
id = ID_UNKNOWN;
return id;
}
/* Use the default register values to identify the Taos device */
static int taos_tsl258x_device(unsigned char *bufp)
{
return ((bufp[TSL258X_CHIPID] & 0xf0) == 0x90);
}
/*
* Client probe function - When a valid device is found, the driver's device
* data structure is updated, and initialization completes successfully.
*/
static int taos_probe(struct i2c_client *clientp,
const struct i2c_device_id *idp)
{
int i, ret = -ENOMEM;
unsigned char buf[TSL258X_MAX_DEVICE_REGS];
struct tsl258x_chip *chip;
if (!i2c_check_functionality(clientp->adapter, I2C_FUNC_SMBUS_BYTE_DATA)) {
dev_err(&clientp->dev, "taos_probe() i2c smbus functions unsupported\n");
return -EOPNOTSUPP;
}
chip = kzalloc(sizeof(struct tsl258x_chip), GFP_KERNEL);
if (chip == NULL) {
dev_err(&clientp->dev, "couldn't allocate memory for chip\n");
return -ENOMEM;
}
chip->client = clientp;
chip->pdata = clientp->dev.platform_data;
i2c_set_clientdata(clientp, chip);
chip->input = input_allocate_device();
/* couldn't allocate input device */
if (!chip->input) {
dev_err(&clientp->dev, "couldn't allocate input device\n");
ret = -ENOMEM;
goto err_input_alloc_failed;
}
/* set up the input device, this name is the dev-node-name under /dev/input/ */
chip->input->name = "tsl2584 ambient light sensor";
input_set_capability(chip->input, EV_MSC, MSC_RAW);
/* attempt to register the input device */
ret = input_register_device(chip->input);
if (ret < 0) {
dev_err(&clientp->dev, "input device register failed:%d\n", ret);
goto err_input_register_failed;
}
/* Register sysfs hooks */
ret = sysfs_create_group(&clientp->dev.kobj, &tsl258x_attribute_group);
if (ret) {
dev_err(&clientp->dev, "sysfs_create_group failed: %d\n", ret);
goto err_sysfs_failed;
}
tsl258x_wq = create_workqueue("tsl258x");
if (!tsl258x_wq) {
dev_err(&clientp->dev, "work queue create failed:%d\n", ret);
ret = -ENOMEM;
goto err_tsl_wq_failed;
}
INIT_WORK(&chip->data_work, taos_workqueue_handler);
setup_timer(&chip->timer, taos_timer_handler, (unsigned long)chip);
mutex_init(&chip->als_mutex);
chip->als_status = TSL258X_STATUS_UNKNOWN;
if (((struct tsl258x_platform_data *)chip->pdata)->gpio_conf)
((struct tsl258x_platform_data *)chip->pdata)->gpio_conf();
for (i = 0; i < TSL258X_MAX_DEVICE_REGS; i++) {
ret = taos_i2c_smbus_write(clientp,
(TSL258X_CMD_REG | (TSL258X_CNTRL + i)));
if (ret < 0) {
dev_err(&clientp->dev, "taos_i2c_smbus_write() to cmd "
"reg failed in taos_probe(), err = %d\n", ret);
goto err_tsl_hw_failed;
}
ret = taos_i2c_smbus_read(clientp);
if (ret < 0) {
dev_err(&clientp->dev, "taos_i2c_smbus_read from "
"reg failed in taos_probe(), err = %d\n", ret);
goto err_tsl_hw_failed;
}
buf[i] = ret;
}
if (!taos_tsl258x_device(buf)) {
dev_info(&clientp->dev, "i2c device found but does not match "
"expected id in taos_probe()\n");
ret = -ENODEV;
goto err_tsl_hw_failed;
}
if (idp->driver_data != ID_UNKNOWN)
chip->id = (int) idp->driver_data;
else
chip->id = taos_tsl258x_chip_id(buf);
ret = taos_i2c_smbus_write(clientp, (TSL258X_CMD_REG | TSL258X_CNTRL));
if (ret < 0) {
dev_err(&clientp->dev, "taos_i2c_smbus_write() to cmd reg "
"failed in taos_probe(), err = %d\n", ret);
goto err_tsl_hw_failed;
}
/* Load up the V2 defaults (these are hard coded defaults for now) */
taos_defaults(chip);
/* Make sure the chip is on */
ret = taos_init_configure(chip, chip->pdata);
if (ret)
goto err_tsl_hw_failed;
pm_runtime_enable(&clientp->dev);
dev_info(&clientp->dev, "ALS found, %s.\n", tsl2583x_get_name(chip));
return 0;
err_tsl_hw_failed:
destroy_workqueue(tsl258x_wq);
err_tsl_wq_failed:
sysfs_remove_group(&clientp->dev.kobj, &tsl258x_attribute_group);
err_sysfs_failed:
input_unregister_device(chip->input);
err_input_register_failed:
/* REVERTME: skip the free device call in case no als is detected */
// input_free_device(chip->input);
err_input_alloc_failed:
kfree(chip);
return ret;
}
#ifdef CONFIG_PM_SLEEP
static int taos_suspend(struct device *dev)
{
struct tsl258x_chip *chip = dev_get_drvdata(dev);
int ret = 0;
mutex_lock(&chip->als_mutex);
if (chip->als_status != TSL258X_STATUS_POWERED_OFF) {
ret = taos_set_power(chip, false);
}
mutex_unlock(&chip->als_mutex);
return ret;
}
static int taos_resume(struct device *dev)
{
struct tsl258x_chip *chip = dev_get_drvdata(dev);
int gain;
int ret = 0;
mutex_lock(&chip->als_mutex);
if (chip->als_status == TSL258X_STATUS_POWERED_OFF) {
ret = taos_set_power(chip, true);
if (ret)
goto error;
ret = taos_set_als_time(chip, chip->taos_settings.als_time);
if (ret)
goto error;
gain = tsl2584_als_gain_tbl[chip->taos_settings.als_gain_idex].gain_val;
ret = taos_set_gain(chip, gain);
}
error:
mutex_unlock(&chip->als_mutex);
return ret;
}
#endif /* CONFIG_PM_SLEEP */
static int taos_remove(struct i2c_client *client)
{
struct tsl258x_chip *chip = i2c_get_clientdata(client);
pm_runtime_disable(&client->dev);
if (tsl258x_wq)
destroy_workqueue(tsl258x_wq);
del_timer(&chip->timer);
sysfs_remove_group(&client->dev.kobj, &tsl258x_attribute_group);
if (chip->input) {
input_unregister_device(chip->input);
input_free_device(chip->input);
}
kfree(chip);
return 0;
}
static struct i2c_device_id taos_idtable[] = {
{ "tsl258x", ID_UNKNOWN },
{ "tsl2580", ID_TSL2580 },
{ "tsl2581", ID_TSL2581 },
{ "tsl2583", ID_TSL2583 },
{ "tsl2584", ID_TSL2584TSV },
{}
};
MODULE_DEVICE_TABLE(i2c, taos_idtable);
static char *tsl2583x_get_name(struct tsl258x_chip *chip)
{
int i;
for (i = 0; i < sizeof(taos_idtable) / sizeof(taos_idtable[0]); i++) {
if ((int) taos_idtable[i].driver_data == chip->id)
return taos_idtable[i].name;
}
return "unknown sensor";
}
SIMPLE_DEV_PM_OPS(taos_pm_ops, taos_suspend, taos_resume);
/* Driver definition */
static struct i2c_driver taos_driver = {
.driver = {
.name = TSL258X_NAME,
.pm = &taos_pm_ops,
},
.id_table = taos_idtable,
.probe = taos_probe,
.remove = taos_remove,
};
static int __init taos_init(void)
{
return i2c_add_driver(&taos_driver);
}
static void __exit taos_exit(void)
{
i2c_del_driver(&taos_driver);
}
module_init(taos_init);
module_exit(taos_exit);
MODULE_AUTHOR("J. August Brenner<jbrenner@taosinc.com>");
MODULE_DESCRIPTION("TAOS tsl258x ambient light sensor driver");
MODULE_LICENSE("GPL");