Signed-off-by: Peter Meerwald <pmeerw@xxxxxxxxxx> --- drivers/iio/light/Kconfig | 13 + drivers/iio/light/si114x.c | 1239 ++++++++++++++++++++++++++++++++++++++ include/linux/iio/light/si114x.h | 21 + 3 files changed, 1273 insertions(+) create mode 100644 drivers/iio/light/si114x.c create mode 100644 include/linux/iio/light/si114x.h diff --git a/drivers/iio/light/Kconfig b/drivers/iio/light/Kconfig index 91d15d2..1af945b 100644 --- a/drivers/iio/light/Kconfig +++ b/drivers/iio/light/Kconfig @@ -32,6 +32,19 @@ config SENSORS_LM3533 changes. The ALS-control output values can be set per zone for the three current output channels. +config SI114X + tristate "SI114x combined ALS and proximity sensor" + depends on I2C + default n + ---help--- + Say Y here if you want to build a driver for the Silicon Labs SI114x + combined ambient light and proximity sensor chips (SI1141, SI1142, + SI1143). The driver supports forced (with and w/o IRQ) and autonomous + measurements (with IRQ only). + + To compile this driver as a module, choose M here: the + module will be called si114x. + config VCNL4000 tristate "VCNL4000 combined ALS and proximity sensor" depends on I2C diff --git a/drivers/iio/light/si114x.c b/drivers/iio/light/si114x.c new file mode 100644 index 0000000..9ab0d4d --- /dev/null +++ b/drivers/iio/light/si114x.c @@ -0,0 +1,1239 @@ +/* + * si114x.c - Support for Silabs si114x combined ambient light and + * proximity sensor + * + * Copyright 2012 Peter Meerwald <pmeerw@xxxxxxxxxx> + * + * This file is subject to the terms and conditions of version 2 of + * the GNU General Public License. See the file COPYING in the main + * directory of this archive for more details. + * + * IIO driver for si114x (7-bit I2C slave address 0x5a) + * + * driver supports IRQ and non-IRQ mode; an IRQ is required for + * autonomous measurement mode + * TODO: + * thresholds + * power management (measurement rate zero) + */ + +#include <linux/module.h> +#include <linux/i2c.h> +#include <linux/err.h> +#include <linux/slab.h> +#include <linux/delay.h> +#include <linux/irq.h> +#include <linux/gpio.h> + +#include <linux/iio/iio.h> +#include <linux/iio/sysfs.h> +#include <linux/iio/trigger.h> +#include <linux/iio/trigger_consumer.h> +#include <linux/iio/triggered_buffer.h> +#include <linux/iio/buffer.h> + +#include "si114x.h" + +#define SI114X_REG_PART_ID 0x00 +#define SI114X_REG_REV_ID 0x01 +#define SI114X_REG_SEQ_ID 0x02 +#define SI114X_REG_INT_CFG 0x03 +#define SI114X_REG_IRQ_ENABLE 0x04 +#define SI114X_REG_IRQ_MODE1 0x05 +#define SI114X_REG_IRQ_MODE2 0x06 +#define SI114X_REG_HW_KEY 0x07 +/* RATE stores a 16 bit value compressed to 8 bit */ +#define SI114X_REG_MEAS_RATE 0x08 +#define SI114X_REG_ALS_RATE 0x09 +#define SI114X_REG_PS_RATE 0x0a +#define SI114X_REG_ALS_LOW_TH0 0x0b +#define SI114X_REG_ALS_LOW_TH1 0x0c +#define SI114X_REG_ALS_HI_TH0 0x0d +#define SI114X_REG_ALS_HI_TH1 0x0e +#define SI114X_REG_PS_LED21 0x0f +#define SI114X_REG_PS_LED3 0x10 +/* + * for rev A10 and below TH0 stores a 16 bit value compressed to 8 bit and + * TH1 is not used; newer revision have the LSB in TH0 and the MSB in TH1 + */ +#define SI114X_REG_PS1_TH0 0x11 +#define SI114X_REG_PS1_TH1 0x12 +#define SI114X_REG_PS2_TH0 0x13 +#define SI114X_REG_PS2_TH1 0x11 +#define SI114X_REG_PS3_TH0 0x15 +#define SI114X_REG_PS3_TH1 0x16 +#define SI114X_REG_PARAM_WR 0x17 +#define SI114X_REG_COMMAND 0x18 +#define SI114X_REG_RESPONSE 0x20 +#define SI114X_REG_IRQ_STATUS 0x21 +#define SI114X_REG_ALSVIS_DATA0 0x22 +#define SI114X_REG_ALSVIS_DATA1 0x23 +#define SI114X_REG_ALSIR_DATA0 0x24 +#define SI114X_REG_ALSIR_DATA1 0x25 +#define SI114X_REG_PS1_DATA0 0x26 +#define SI114X_REG_PS1_DATA1 0x27 +#define SI114X_REG_PS2_DATA0 0x28 +#define SI114X_REG_PS2_DATA1 0x29 +#define SI114X_REG_PS3_DATA0 0x2a +#define SI114X_REG_PS3_DATA1 0x2b +#define SI114X_REG_AUX_DATA0 0x2c +#define SI114X_REG_AUX_DATA1 0x2d +#define SI114X_REG_PARAM_RD 0x2e +#define SI114X_REG_CHIP_STAT 0x30 + +/* Parameter offsets */ +#define SI114X_PARAM_I2C_ADDR 0x00 +#define SI114X_PARAM_CHLIST 0x01 +#define SI114X_PARAM_PSLED12_SELECT 0x02 +#define SI114X_PARAM_PSLED3_SELECT 0x03 +#define SI114X_PARAM_FILTER_EN 0x04 +#define SI114X_PARAM_PS_ENCODING 0x05 +#define SI114X_PARAM_ALS_ENCODING 0x06 +#define SI114X_PARAM_PS1_ADC_MUX 0x07 +#define SI114X_PARAM_PS2_ADC_MUX 0x08 +#define SI114X_PARAM_PS3_ADC_MUX 0x09 +#define SI114X_PARAM_PS_ADC_COUNTER 0x0a +#define SI114X_PARAM_PS_ADC_GAIN 0x0b +#define SI114X_PARAM_PS_ADC_MISC 0x0c +#define SI114X_PARAM_ALS_ADC_MUX 0x0d +#define SI114X_PARAM_ALSIR_ADC_MUX 0x0e +#define SI114X_PARAM_AUX_ADC_MUX 0x0f +#define SI114X_PARAM_ALSVIS_ADC_COUNTER 0x10 +#define SI114X_PARAM_ALSVIS_ADC_GAIN 0x11 +#define SI114X_PARAM_ALSVIS_ADC_MISC 0x12 +#define SI114X_PARAM_ALS_HYST 0x16 +#define SI114X_PARAM_PS_HYST 0x17 +#define SI114X_PARAM_PS_HISTORY 0x18 +#define SI114X_PARAM_ALS_HISTORY 0x19 +#define SI114X_PARAM_ADC_OFFSET 0x1a +#define SI114X_PARAM_SLEEP_CTRL 0x1b +#define SI114X_PARAM_LED_RECOVERY 0x1c +#define SI114X_PARAM_ALSIR_ADC_COUNTER 0x1d +#define SI114X_PARAM_ALSIR_ADC_GAIN 0x1e +#define SI114X_PARAM_ALSIR_ADC_MISC 0x1f + +/* Channel enable masks for CHLIST parameter */ +#define SI114X_CHLIST_EN_PS1 0x01 +#define SI114X_CHLIST_EN_PS2 0x02 +#define SI114X_CHLIST_EN_PS3 0x04 +#define SI114X_CHLIST_EN_ALSVIS 0x10 +#define SI114X_CHLIST_EN_ALSIR 0x20 +#define SI114X_CHLIST_EN_AUX 0x40 + +/* Signal range mask for ADC_MISC parameter */ +#define SI114X_MISC_RANGE 0x20 + +/* Commands for REG_COMMAND */ +#define SI114X_COMMAND_NOP 0x00 +#define SI114X_COMMAND_RESET 0x01 +#define SI114X_COMMAND_BUSADDR 0x02 +#define SI114X_COMMAND_PS_FORCE 0x05 +#define SI114X_COMMAND_ALS_FORCE 0x06 +#define SI114X_COMMAND_PSALS_FORCE 0x07 +#define SI114X_COMMAND_PS_PAUSE 0x09 +#define SI114X_COMMAND_ALS_PAUSE 0x0a +#define SI114X_COMMAND_PSALS_PAUSE 0x0b +#define SI114X_COMMAND_PS_AUTO 0x0d +#define SI114X_COMMAND_ALS_AUTO 0x0e +#define SI114X_COMMAND_PSALS_AUTO 0x0f +#define SI114X_COMMAND_PARAM_QUERY 0x80 +#define SI114X_COMMAND_PARAM_SET 0xa0 +#define SI114X_COMMAND_PARAM_AND 0xc0 +#define SI114X_COMMAND_PARAM_OR 0xe0 + +/* Interrupt configuration masks for INT_CFG register */ +#define SI114x_INT_CFG_OE 0x01 /* enable interrupt */ +#define SI114x_INT_CFG_MODE 0x02 /* auto reset interrupt pin */ + +/* Interrupt enable masks for IRQ_ENABLE register */ +#define SI114X_CMD_IE 0x20 +#define SI114X_PS3_IE 0x10 +#define SI114X_PS2_IE 0x08 +#define SI114X_PS1_IE 0x04 +#define SI114X_ALS_INT1_IE 0x02 +#define SI114X_ALS_INT0_IE 0x01 + +/* Interrupt mode masks for IRQ_MODE1 register */ +#define SI114X_PS2_IM_GREATER 0xc0 +#define SI114X_PS2_IM_CROSS 0x40 +#define SI114X_PS1_IM_GREATER 0x30 +#define SI114X_PS1_IM_CROSS 0x10 + +/* Interrupt mode masks for IRQ_MODE2 register */ +#define SI114X_CMD_IM_ERROR 0x04 +#define SI114X_PS3_IM_GREATER 0x03 +#define SI114X_PS3_IM_CROSS 0x01 + +/* Measurement rate settings */ +#define SI114X_MEAS_RATE_FORCED 0x00 +#define SI114X_MEAS_RATE_10MS 0x84 +#define SI114X_MEAS_RATE_20MS 0x94 +#define SI114X_MEAS_RATE_100MS 0xb9 +#define SI114X_MEAS_RATE_496MS 0xdf +#define SI114X_MEAS_RATE_1984MS 0xff + +/* ALS rate settings relative to measurement rate */ +#define SI114X_ALS_RATE_OFF 0x00 +#define SI114X_ALS_RATE_1X 0x08 +#define SI114X_ALS_RATE_10X 0x32 +#define SI114X_ALS_RATE_100X 0x69 + +/* PS rate settings relative to measurement rate */ +#define SI114X_PS_RATE_OFF 0x00 +#define SI114X_PS_RATE_1X 0x08 +#define SI114X_PS_RATE_10X 0x32 +#define SI114X_PS_RATE_100X 0x69 + +/* Sequencer revision from SEQ_ID */ +#define SI114X_SEQ_REV_A01 0x01 +#define SI114X_SEQ_REV_A02 0x02 +#define SI114X_SEQ_REV_A03 0x03 +#define SI114X_SEQ_REV_A10 0x08 +#define SI114X_SEQ_REV_A11 0x09 + +#define SI114X_DRV_NAME "si114x" + +struct si114x_data { + struct i2c_client *client; + struct iio_info *info; + u8 part; + u8 rev; + u8 seq; + u16 *buffer; + bool ps_active; + bool als_active; + wait_queue_head_t data_avail; + bool got_data; + const struct si114x_platform_data *pdata; + bool use_irq; + struct iio_trigger *trig; +}; + +static const struct i2c_device_id si114x_id[] = { + { "si114x", 0 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, si114x_id); + +static u16 si114x_uncompress(u8 x) +{ + u16 result = 0; + u8 exponent = 0; + + if (x < 8) + return 0; + + exponent = (x & 0xf0) >> 4; + result = 0x10 | (x & 0x0f); + + if (exponent >= 4) + return result << (exponent - 4); + return result >> (4 - exponent); +} + +static u8 si114x_compress(u16 x) +{ + u32 exponent = 0; + u32 significand = 0; + u32 tmp = x; + + if (x == 0x0000) + return 0x00; + if (x == 0x0001) + return 0x08; + + while (1) { + tmp >>= 1; + exponent += 1; + if (tmp == 1) + break; + } + + if (exponent < 5) { + significand = x << (4 - exponent); + return (exponent << 4) | (significand & 0xF); + } + + significand = x >> (exponent - 5); + if (significand & 1) { + significand += 2; + if (significand & 0x0040) { + exponent += 1; + significand >>= 1; + } + } + + return (exponent << 4) | ((significand >> 1) & 0xF); +} + +static int si114x_param_op(struct si114x_data *data, u8 op, u8 param, u8 value) +{ + struct i2c_client *client = data->client; + int ret; + + ret = i2c_smbus_write_byte_data(client, SI114X_REG_PARAM_WR, value); + if (ret < 0) + return ret; + ret = i2c_smbus_write_byte_data(client, SI114X_REG_COMMAND, + op | (param & 0x1F)); + if (ret < 0) + return ret; + + return 0; +} + +static int si114x_param_set(struct si114x_data *data, u8 param, u8 value) +{ + return si114x_param_op(data, SI114X_COMMAND_PARAM_SET, param, value); +} + +static int si114x_param_or(struct si114x_data *data, u8 param, u8 value) +{ + return si114x_param_op(data, SI114X_COMMAND_PARAM_OR, param, value); +} + +static int si114x_param_and(struct si114x_data *data, u8 param, u8 value) +{ + return si114x_param_op(data, SI114X_COMMAND_PARAM_AND, param, value); +} + +static int si114x_param_get(struct si114x_data *data, u8 param) +{ + struct i2c_client *client = data->client; + int ret; + + ret = i2c_smbus_write_byte_data(client, SI114X_REG_COMMAND, + SI114X_COMMAND_PARAM_QUERY | (param & 0x1F)); + if (ret < 0) + return ret; + ret = i2c_smbus_read_byte_data(client, SI114X_REG_PARAM_RD); + if (ret < 0) + return ret; + + return ret & 0xff; +} + +static irqreturn_t si114x_trigger_handler(int irq, void *private) +{ + struct iio_poll_func *pf = private; + struct iio_dev *indio_dev = pf->indio_dev; + struct si114x_data *data = iio_priv(indio_dev); + struct iio_buffer *buffer = indio_dev->buffer; + s64 time_ns = iio_get_time_ns(); + int len = 0; + int i, j = 0; + + int ret = i2c_smbus_write_byte_data(data->client, + SI114X_REG_COMMAND, SI114X_COMMAND_PSALS_FORCE); + if (ret < 0) + goto done; + + for_each_set_bit(i, indio_dev->active_scan_mask, + indio_dev->masklength) { + ret = i2c_smbus_read_word_data(data->client, + indio_dev->channels[i].address); + if (ret < 0) + goto done; + + data->buffer[j++] = ret & 0xffff; + len += 2; + } + + if (indio_dev->scan_timestamp) + *(s64 *)((u8 *)data->buffer + ALIGN(len, sizeof(s64))) + = time_ns; + iio_push_to_buffer(buffer, (u8 *)data->buffer, time_ns); + +done: + iio_trigger_notify_done(indio_dev->trig); + + return IRQ_HANDLED; +} + +static irqreturn_t si114x_irq(int irq, void *private) +{ + struct iio_dev *indio_dev = private; + struct si114x_data *data = iio_priv(indio_dev); + int ret; + + ret = i2c_smbus_read_byte_data(data->client, + SI114X_REG_IRQ_STATUS); + if (ret < 0 || !(ret & (SI114X_PS3_IE | SI114X_PS2_IE | SI114X_PS1_IE | + SI114X_ALS_INT1_IE | SI114X_ALS_INT0_IE))) + return IRQ_HANDLED; + + if (iio_buffer_enabled(indio_dev)) { + iio_trigger_poll(indio_dev->trig, 0); + } else { + data->got_data = true; + wake_up_interruptible(&data->data_avail); + } + + /* clearing IRQ */ + ret = i2c_smbus_write_byte_data(data->client, + SI114X_REG_IRQ_STATUS, ret & 0x1f); + if (ret < 0) + dev_err(&data->client->dev, "clearing irq failed\n"); + + return IRQ_HANDLED; +} + +static int si114x_trig_set_state(struct iio_trigger *trig, bool state) +{ + struct iio_dev *indio_dev = trig->private_data; + struct si114x_data *data = iio_priv(indio_dev); + int ret = 0; + int cmd; + + /* configure autonomous mode */ + cmd = state ? SI114X_COMMAND_PSALS_AUTO : + SI114X_COMMAND_PSALS_PAUSE; + + ret = i2c_smbus_write_byte_data(data->client, + SI114X_REG_COMMAND, cmd); + + return ret; +} + +static const struct iio_trigger_ops si114x_trigger_ops = { + .owner = THIS_MODULE, + .set_trigger_state = si114x_trig_set_state, +}; + +static int si114x_probe_trigger(struct iio_dev *indio_dev) +{ + struct si114x_data *data = iio_priv(indio_dev); + int ret; + + data->trig = iio_trigger_alloc("si114x-dev%d", indio_dev->id); + if (!data->trig) + return -ENOMEM; + + data->trig->dev.parent = &data->client->dev; + data->trig->ops = &si114x_trigger_ops; + data->trig->private_data = indio_dev; + ret = iio_trigger_register(data->trig); + if (ret) + goto error_free_trig; + + /* select default trigger */ + indio_dev->trig = data->trig; + + return 0; + +error_free_trig: + iio_trigger_free(data->trig); + return ret; +} + +static void si114x_remove_trigger(struct iio_dev *indio_dev) +{ + struct si114x_data *data = iio_priv(indio_dev); + + iio_trigger_unregister(data->trig); + iio_trigger_free(data->trig); +} + +static int si114x_read_raw(struct iio_dev *indio_dev, + struct iio_chan_spec const *chan, + int *val, int *val2, long mask) +{ + struct si114x_data *data = iio_priv(indio_dev); + int ret = -EINVAL; + u8 cmd, reg, shift; + + switch (mask) { + case IIO_CHAN_INFO_RAW: + switch (chan->type) { + case IIO_INTENSITY: + case IIO_PROXIMITY: + case IIO_TEMP: + if (iio_buffer_enabled(indio_dev)) + return -EBUSY; + + if (chan->type == IIO_PROXIMITY) + cmd = SI114X_COMMAND_PS_FORCE; + else + cmd = SI114X_COMMAND_ALS_FORCE; + ret = i2c_smbus_write_byte_data(data->client, + SI114X_REG_COMMAND, cmd); + if (ret < 0) + return ret; + if (data->use_irq) { + + ret = wait_event_interruptible_timeout( + data->data_avail, data->got_data, + msecs_to_jiffies(1000)); + data->got_data = false; + if (ret == 0) + ret = -ETIMEDOUT; + + } else + msleep(20); + + if (ret < 0) + return ret; + + ret = i2c_smbus_read_word_data(data->client, + chan->address); + if (ret < 0) + return ret; + + *val = ret & 0xffff; + + ret = IIO_VAL_INT; + break; + case IIO_CURRENT: + shift = 0; + switch (chan->channel) { + case 2: + reg = SI114X_REG_PS_LED3; + break; + case 1: + shift = 4; + case 0: + reg = SI114X_REG_PS_LED21; + break; + default: + return -EINVAL; + } + + ret = i2c_smbus_read_byte_data(data->client, reg); + if (ret < 0) + return ret; + + *val = (ret >> shift) & 0x0f; + + ret = IIO_VAL_INT; + break; + default: + break; + } + break; + case IIO_CHAN_INFO_HARDWAREGAIN: + if (chan->type == IIO_PROXIMITY) + reg = SI114X_PARAM_PS_ADC_GAIN; + else if (chan->type == IIO_INTENSITY) { + if (chan->channel2 == IIO_MOD_LIGHT_IR) + reg = SI114X_PARAM_ALSIR_ADC_GAIN; + else + reg = SI114X_PARAM_ALSVIS_ADC_GAIN; + } else + return -EINVAL; + + ret = si114x_param_get(data, reg); + if (ret < 0) + return ret; + + *val = ret & 0x07; + + ret = IIO_VAL_INT; + break; + default: + break; + } + + return ret; +} + +static int si114x_write_raw(struct iio_dev *indio_dev, + struct iio_chan_spec const *chan, + int val, int val2, long mask) +{ + struct si114x_data *data = iio_priv(indio_dev); + u8 reg1, reg2, shift; + int ret; + + switch (mask) { + case IIO_CHAN_INFO_HARDWAREGAIN: + if (chan->type == IIO_PROXIMITY) { + if (val < 0 || val > 5) + return -EINVAL; + reg1 = SI114X_PARAM_PS_ADC_GAIN; + reg2 = SI114X_PARAM_PS_ADC_COUNTER; + } else if (chan->type == IIO_INTENSITY) { + if (val < 0 || val > 7) + return -EINVAL; + if (chan->channel2 == IIO_MOD_LIGHT_IR) { + reg1 = SI114X_PARAM_ALSIR_ADC_GAIN; + reg2 = SI114X_PARAM_ALSIR_ADC_COUNTER; + } else { + reg1 = SI114X_PARAM_ALSVIS_ADC_GAIN; + reg2 = SI114X_PARAM_ALSVIS_ADC_COUNTER; + } + } else + return -EINVAL; + + ret = si114x_param_set(data, reg1, val); + if (ret < 0) + return ret; + /* set recovery period to one's complement of gain */ + ret = si114x_param_set(data, reg2, (~val & 0x07) << 4); + return ret; + case IIO_CHAN_INFO_RAW: + if (chan->type != IIO_CURRENT) + return -EINVAL; + + if (val < 0 || val > 0xf) + return -EINVAL; + + shift = 0; + switch (chan->channel) { + case 2: + reg1 = SI114X_REG_PS_LED3; + break; + case 1: + shift = 4; + case 0: + reg1 = SI114X_REG_PS_LED21; + break; + default: + return -EINVAL; + } + + ret = i2c_smbus_read_byte_data(data->client, reg1); + if (ret < 0) + return ret; + ret = i2c_smbus_write_byte_data(data->client, reg1, + (ret & ~(0x0f << shift)) | + ((val & 0x0f) << shift)); + return ret; + default: + break; + } + return -EINVAL; +} + +static int si114x_update_scan_mode(struct iio_dev *indio_dev, + const unsigned long *scan_mask) +{ + struct si114x_data *data = iio_priv(indio_dev); + + kfree(data->buffer); + data->buffer = kmalloc(indio_dev->scan_bytes, GFP_KERNEL); + if (data->buffer == NULL) + return -ENOMEM; + + return 0; +} + +static int si114x_reg_access(struct iio_dev *indio_dev, + unsigned reg, unsigned writeval, + unsigned *readval) +{ + struct si114x_data *data = iio_priv(indio_dev); + int ret; + + if (readval) { + ret = i2c_smbus_read_byte_data(data->client, reg); + if (ret < 0) + return ret; + *readval = ret; + ret = 0; + } else + ret = i2c_smbus_write_byte_data(data->client, reg, writeval); + + return ret; +} + +static int si114x_revisions(struct si114x_data *data) +{ + int ret = i2c_smbus_read_byte_data(data->client, SI114X_REG_PART_ID); + if (ret < 0) + return ret; + + switch (ret) { + case 0x41: + case 0x42: + case 0x43: + data->part = ret; + break; + default: + dev_err(&data->client->dev, "invalid part\n"); + return -EINVAL; + } + + ret = i2c_smbus_read_byte_data(data->client, SI114X_REG_REV_ID); + if (ret < 0) + return ret; + data->rev = ret; + + ret = i2c_smbus_read_byte_data(data->client, SI114X_REG_SEQ_ID); + if (ret < 0) + return ret; + data->seq = ret; + + if (data->seq < SI114X_SEQ_REV_A03) + dev_info(&data->client->dev, "WARNING: old sequencer revision\n"); + + return 0; +} + +static inline unsigned int si114x_leds(struct si114x_data *data) +{ + return data->part - 0x40; +} + +static const struct iio_chan_spec si114x_proximity_channel = { + .type = IIO_PROXIMITY, + .indexed = 1, + .info_mask = IIO_CHAN_INFO_RAW_SEPARATE_BIT | + IIO_CHAN_INFO_HARDWAREGAIN_SHARED_BIT, + .scan_type = IIO_ST('u', 16, 16, 0) +}; + +static const struct iio_chan_spec si114x_current_channel = { + .type = IIO_CURRENT, + .indexed = 1, + .output = 1, + .info_mask = IIO_CHAN_INFO_RAW_SEPARATE_BIT +}; + +static const struct iio_chan_spec si114x_intensity_channel = { + .type = IIO_INTENSITY, + .info_mask = IIO_CHAN_INFO_RAW_SEPARATE_BIT | + IIO_CHAN_INFO_HARDWAREGAIN_SEPARATE_BIT, + .scan_type = IIO_ST('u', 16, 16, 0) +}; + +static const struct iio_chan_spec si114x_temp_channel = { + .type = IIO_TEMP, + .info_mask = IIO_CHAN_INFO_RAW_SEPARATE_BIT, + .scan_type = IIO_ST('u', 16, 16, 0), + .address = SI114X_REG_AUX_DATA0 +}; + +static const struct iio_chan_spec si114x_timestamp_channel = + IIO_CHAN_SOFT_TIMESTAMP(-1); + +static ssize_t si114x_range_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t len) +{ + struct iio_dev *indio_dev = dev_to_iio_dev(dev); + struct iio_dev_attr *dev_attr = to_iio_dev_attr(attr); + struct si114x_data *data = iio_priv(indio_dev); + int ret; + + if (sysfs_streq(buf, "normal")) + ret = si114x_param_and(data, dev_attr->address, + ~SI114X_MISC_RANGE); + else if (sysfs_streq(buf, "high")) + ret = si114x_param_or(data, dev_attr->address, + SI114X_MISC_RANGE); + else + return -EINVAL; + + return ret ? ret : len; +} + +static ssize_t si114x_range_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct iio_dev *indio_dev = dev_to_iio_dev(dev); + struct iio_dev_attr *dev_attr = to_iio_dev_attr(attr); + struct si114x_data *data = iio_priv(indio_dev); + int ret; + + ret = si114x_param_get(data, dev_attr->address); + if (ret < 0) + return ret; + + return sprintf(buf, "%s\n", + (ret & SI114X_MISC_RANGE) ? "high" : "normal"); +} + +static IIO_DEVICE_ATTR(in_proximity_range, S_IRUGO | S_IWUSR, + si114x_range_show, + si114x_range_store, + SI114X_PARAM_PS_ADC_MISC); + +static IIO_DEVICE_ATTR(in_intensity_range, S_IRUGO | S_IWUSR, + si114x_range_show, + si114x_range_store, + SI114X_PARAM_ALSVIS_ADC_MISC); + +static IIO_DEVICE_ATTR(in_intensity_ir_range, S_IRUGO | S_IWUSR, + si114x_range_show, + si114x_range_store, + SI114X_PARAM_ALSIR_ADC_MISC); + +static IIO_CONST_ATTR(in_proximity_range_available, "normal high"); +static IIO_CONST_ATTR(in_intensity_range_available, "normal high"); +static IIO_CONST_ATTR(in_intensity_ir_range_available, "normal high"); + +static int si114x_alloc_channels(struct iio_dev *indio_dev) +{ + unsigned int i, j; + struct si114x_data *data = iio_priv(indio_dev); + unsigned int num_channels = 4 + 2*si114x_leds(data); + struct iio_chan_spec *channels = kcalloc(num_channels, + sizeof(struct iio_chan_spec), GFP_KERNEL); + if (!channels) + return -ENOMEM; + + j = 0; + for (i = 0; i < si114x_leds(data); i++) { + channels[j] = si114x_proximity_channel; + channels[j].channel = i; + channels[j].scan_index = i; + channels[j].address = SI114X_REG_PS1_DATA0 + i*2; + j++; + + channels[j] = si114x_current_channel; + channels[j].channel = i; + channels[j].scan_index = -1; + j++; + } + + channels[j] = si114x_intensity_channel; + channels[j].scan_index = i; + channels[j].address = SI114X_REG_ALSVIS_DATA0; + j++; + + channels[j] = si114x_intensity_channel; + channels[j].scan_index = i+1; + channels[j].modified = 1; + channels[j].channel2 = IIO_MOD_LIGHT_IR; + channels[j].address = SI114X_REG_ALSIR_DATA0; + j++; + + channels[j] = si114x_temp_channel; + channels[j].scan_index = i+2; + j++; + + channels[j] = si114x_timestamp_channel; + channels[j].scan_index = i+3; + j++; + + indio_dev->channels = channels; + indio_dev->num_channels = num_channels; + + return 0; +} + +static int si114x_set_chlist(struct iio_dev *indio_dev, bool all) +{ + struct si114x_data *data = iio_priv(indio_dev); + u8 reg = 0; + int i; + + if (all) { + reg = SI114X_CHLIST_EN_ALSVIS | SI114X_CHLIST_EN_ALSIR | + SI114X_CHLIST_EN_AUX; + switch (si114x_leds(data)) { + case 3: + reg |= SI114X_CHLIST_EN_PS3; + case 2: + reg |= SI114X_CHLIST_EN_PS2; + case 1: + reg |= SI114X_CHLIST_EN_PS1; + break; + } + } else + for_each_set_bit(i, indio_dev->active_scan_mask, + indio_dev->masklength) { + switch (indio_dev->channels[i].address) { + case SI114X_REG_ALSVIS_DATA0: + reg |= SI114X_CHLIST_EN_ALSVIS; + break; + case SI114X_REG_ALSIR_DATA0: + reg |= SI114X_CHLIST_EN_ALSIR; + break; + case SI114X_REG_PS1_DATA0: + reg |= SI114X_CHLIST_EN_PS1; + break; + case SI114X_REG_PS2_DATA0: + reg |= SI114X_CHLIST_EN_PS2; + break; + case SI114X_REG_PS3_DATA0: + reg |= SI114X_CHLIST_EN_PS3; + break; + case SI114X_REG_AUX_DATA0: + reg |= SI114X_CHLIST_EN_AUX; + break; + } + } + + return si114x_param_set(data, SI114X_PARAM_CHLIST, reg); +} + +static int si114x_initialize(struct iio_dev *indio_dev) +{ + struct si114x_data *data = iio_priv(indio_dev); + struct i2c_client *client = data->client; + u8 reg; + int ret; + + /* send reset command */ + ret = i2c_smbus_write_byte_data(client, SI114X_REG_COMMAND, + SI114X_COMMAND_RESET); + if (ret < 0) + return ret; + + /* hardware key, magic value */ + ret = i2c_smbus_write_byte_data(client, SI114X_REG_HW_KEY, 0x17); + if (ret < 0) + return ret; + + /* interrupt configuration, interrupt output enable */ + ret = i2c_smbus_write_byte_data(client, SI114X_REG_INT_CFG, + data->use_irq ? SI114x_INT_CFG_OE : 0); + if (ret < 0) + return ret; + + /* enable interrupt for certain activities */ + ret = i2c_smbus_write_byte_data(client, SI114X_REG_IRQ_ENABLE, + SI114X_PS3_IE | SI114X_PS2_IE | SI114X_PS1_IE | + SI114X_ALS_INT0_IE); + if (ret < 0) + return ret; + + /* configure interrupt mode for PS1 & PS2 (fire on measurement) */ + ret = i2c_smbus_write_byte_data(client, SI114X_REG_IRQ_MODE1, 0); + if (ret < 0) + return ret; + + /* configure interrupt mode for PS3 (fire on measurement) */ + ret = i2c_smbus_write_byte_data(client, SI114X_REG_IRQ_MODE2, 0); + if (ret < 0) + return ret; + + /* in autonomous mode, wakeup every 100 ms */ + ret = i2c_smbus_write_byte_data(client, SI114X_REG_MEAS_RATE, + SI114X_MEAS_RATE_100MS); + if (ret < 0) + return ret; + + /* measure ALS every time device wakes up */ + ret = i2c_smbus_write_byte_data(client, SI114X_REG_ALS_RATE, + SI114X_ALS_RATE_1X); + if (ret < 0) + return ret; + + /* measure proximity every time device wakes up */ + ret = i2c_smbus_write_byte_data(client, SI114X_REG_PS_RATE, + SI114X_PS_RATE_1X); + if (ret < 0) + return ret; + + /* set LED currents to maximum */ + reg = 0; + switch (si114x_leds(data)) { + case 3: + ret = i2c_smbus_write_byte_data(client, + SI114X_REG_PS_LED3, 0x0f); + if (ret < 0) + return ret; + case 2: + reg = 0xf0; + case 1: + reg |= 0x0f; + ret = i2c_smbus_write_byte_data(client, + SI114X_REG_PS_LED21, reg); + if (ret < 0) + return ret; + break; + } + + ret = si114x_set_chlist(indio_dev, true); + if (ret < 0) + return ret; + + /* set normal proximity measurement mode, set high signal range + * PS measurement */ + ret = si114x_param_set(data, SI114X_PARAM_PS_ADC_MISC, 0x20 | 0x04); + if (ret < 0) + return ret; + + return 0; +} + +static ssize_t si114x_read_frequency(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct iio_dev *indio_dev = dev_to_iio_dev(dev); + struct si114x_data *data = iio_priv(indio_dev); + int ret; + u16 rate; + + ret = i2c_smbus_read_byte_data(data->client, SI114X_REG_MEAS_RATE); + if (ret < 0) + return ret; + + if (ret == 0) + rate = 0; + else + rate = 32000 / si114x_uncompress(ret); + + return sprintf(buf, "%d\n", rate); +} + +static ssize_t si114x_write_frequency(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t len) +{ + struct iio_dev *indio_dev = dev_to_iio_dev(dev); + struct si114x_data *data = iio_priv(indio_dev); + unsigned long val; + int ret; + u8 rate; + + ret = kstrtoul(buf, 10, &val); + if (ret) + return ret; + + switch (val) { + case 250: + case 100: + case 50: + case 25: + case 10: + case 5: + case 2: + case 1: + rate = si114x_compress(32000 / val); + break; + case 0: + rate = 0; + break; + default: + return -EINVAL; + } + + ret = i2c_smbus_write_byte_data(data->client, SI114X_REG_MEAS_RATE, + rate); + + return ret ? ret : len; +} + +static IIO_DEV_ATTR_SAMP_FREQ(S_IWUSR | S_IRUGO, + si114x_read_frequency, + si114x_write_frequency); + +static IIO_CONST_ATTR_SAMP_FREQ_AVAIL("1 2 5 10 25 50 100 250"); + +static const struct attribute *si114x_attrs_general[] = { + &iio_dev_attr_in_proximity_range.dev_attr.attr, + &iio_const_attr_in_proximity_range_available.dev_attr.attr, + &iio_dev_attr_in_intensity_range.dev_attr.attr, + &iio_const_attr_in_intensity_range_available.dev_attr.attr, + &iio_dev_attr_in_intensity_ir_range.dev_attr.attr, + &iio_const_attr_in_intensity_ir_range_available.dev_attr.attr, +}; + +static const struct attribute *si114x_attrs_irq_only[] = { + &iio_dev_attr_sampling_frequency.dev_attr.attr, + &iio_const_attr_sampling_frequency_available.dev_attr.attr, +}; + +static int si114x_alloc_info(struct iio_dev *indio_dev) +{ + struct si114x_data *data = iio_priv(indio_dev); + struct attribute **attrs; + struct attribute_group *attr_group; + struct iio_info *info; + int num_attrs_general = ARRAY_SIZE(si114x_attrs_general); + int num_attrs_irq; + int ret; + + num_attrs_irq = data->use_irq ? + ARRAY_SIZE(si114x_attrs_irq_only) : 0; + + info = kzalloc(sizeof(struct iio_info), GFP_KERNEL); + if (!info) + return -ENOMEM; + indio_dev->info = data->info = info; + + info->read_raw = si114x_read_raw; + info->write_raw = si114x_write_raw; + info->update_scan_mode = si114x_update_scan_mode; + info->debugfs_reg_access = si114x_reg_access; + info->driver_module = THIS_MODULE; + + attr_group = kzalloc(sizeof(struct attribute_group), GFP_KERNEL); + if (!attr_group) { + ret = -ENOMEM; + goto error_free_info; + } + info->attrs = attr_group; + + attrs = kcalloc(num_attrs_general + num_attrs_irq + 1, + sizeof(struct attribute *), GFP_KERNEL); + if (!attrs) { + ret = -ENOMEM; + goto error_free_attr_group; + } + attr_group->attrs = attrs; + + memcpy(&attrs[0], si114x_attrs_general, + num_attrs_general * sizeof(struct attribute *)); + memcpy(&attrs[num_attrs_general], si114x_attrs_irq_only, + num_attrs_irq * sizeof(struct attribute *)); + + return 0; + +error_free_attr_group: + kfree(attr_group); +error_free_info: + kfree(info); + return ret; +} + +static void si114x_free_info(struct iio_info *info) +{ + kfree(info->attrs->attrs); + kfree(info->attrs); + kfree(info); +} + +static int si114x_buffer_postenable(struct iio_dev *indio_dev) +{ + int ret; + + ret = iio_triggered_buffer_postenable(indio_dev); + if (ret < 0) + return ret; + + return si114x_set_chlist(indio_dev, false); +} + +static int si114x_buffer_predisable(struct iio_dev *indio_dev) +{ + int ret; + + ret = si114x_set_chlist(indio_dev, true); + if (ret < 0) + return ret; + + return iio_triggered_buffer_predisable(indio_dev); +} + +static const struct iio_buffer_setup_ops si114x_buffer_setup_ops = { + .preenable = iio_sw_buffer_preenable, + .postenable = si114x_buffer_postenable, + .predisable = si114x_buffer_predisable +}; + +static int __devinit si114x_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct si114x_data *data; + struct iio_dev *indio_dev; + int ret; + + indio_dev = iio_device_alloc(sizeof(*data)); + if (!indio_dev) + return -ENOMEM; + + data = iio_priv(indio_dev); + i2c_set_clientdata(client, indio_dev); + data->client = client; + data->pdata = client->dev.platform_data; + if (data->pdata) + data->use_irq = data->pdata->use_irq; + + ret = si114x_revisions(data); + if (ret < 0) + goto error_free_dev; + + dev_info(&client->dev, "Si11%02x Ambient light/proximity sensor, Rev %02x, Seq: %02x\n", + data->part, data->rev, data->seq); + + ret = si114x_alloc_channels(indio_dev); + if (ret < 0) + goto error_free_dev; + + ret = si114x_alloc_info(indio_dev); + if (ret < 0) + goto error_free_channels; + + indio_dev->dev.parent = &client->dev; + indio_dev->name = SI114X_DRV_NAME; + indio_dev->modes = INDIO_DIRECT_MODE; + init_waitqueue_head(&data->data_avail); + + ret = iio_triggered_buffer_setup(indio_dev, NULL, + si114x_trigger_handler, NULL); + if (ret < 0) + goto error_free_info; + + if (data->use_irq) { + ret = request_threaded_irq(client->irq, + NULL, si114x_irq, + data->pdata->irq_flags | IRQF_ONESHOT, + "si114x_irq", indio_dev); + if (ret < 0) { + dev_err(&client->dev, "irq request failed\n"); + goto error_free_buffer; + } + + ret = si114x_probe_trigger(indio_dev); + if (ret < 0) + goto error_free_irq; + } else + dev_info(&client->dev, "no platform data or irq, using polling\n"); + + ret = si114x_initialize(indio_dev); + if (ret < 0) + goto error_free_trigger; + + ret = iio_device_register(indio_dev); + if (ret < 0) + goto error_free_trigger; + + return 0; + +error_free_trigger: + if (data->use_irq) + si114x_remove_trigger(indio_dev); +error_free_irq: + if (data->use_irq) + free_irq(client->irq, indio_dev); +error_free_buffer: + iio_triggered_buffer_cleanup(indio_dev); +error_free_info: + si114x_free_info(data->info); +error_free_channels: + kfree(indio_dev->channels); +error_free_dev: + iio_device_free(indio_dev); + return ret; +} + +static int __devexit si114x_remove(struct i2c_client *client) +{ + struct iio_dev *indio_dev = i2c_get_clientdata(client); + struct si114x_data *data = iio_priv(indio_dev); + + iio_device_unregister(indio_dev); + iio_triggered_buffer_cleanup(indio_dev); + kfree(data->buffer); + si114x_free_info(data->info); + kfree(indio_dev->channels); + if (data->use_irq) { + si114x_remove_trigger(indio_dev); + free_irq(client->irq, indio_dev); + } + iio_device_free(indio_dev); + + return 0; +} + +static struct i2c_driver si114x_driver = { + .driver = { + .name = SI114X_DRV_NAME, + .owner = THIS_MODULE, + }, + .probe = si114x_probe, + .remove = __devexit_p(si114x_remove), + .id_table = si114x_id, +}; + +module_i2c_driver(si114x_driver); + +MODULE_AUTHOR("Peter Meerwald <pmeerw@xxxxxxxxxx>"); +MODULE_DESCRIPTION("Silabs si114x proximity/ambient light sensor driver"); +MODULE_LICENSE("GPL"); diff --git a/include/linux/iio/light/si114x.h b/include/linux/iio/light/si114x.h new file mode 100644 index 0000000..5e04e7a --- /dev/null +++ b/include/linux/iio/light/si114x.h @@ -0,0 +1,21 @@ +/* + * si114x.h - Support for Silabs si114x combined ambient light and + * proximity sensor + * + * Copyright 2012 Peter Meerwald <pmeerw@xxxxxxxxxx> + * + * This file is subject to the terms and conditions of version 2 of + * the GNU General Public License. See the file COPYING in the main + * directory of this archive for more details. + */ + +#ifndef SI114X_H +#define SI114X_H + +struct si114x_platform_data { + bool use_irq; + int irq_flags; +}; + +#endif /* SI114X_H */ + -- 1.7.9.5 -- To unsubscribe from this list: send the line "unsubscribe linux-iio" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html