diff options
author | Dmitry Torokhov <dmitry.torokhov@gmail.com> | 2019-07-10 23:24:10 -0700 |
---|---|---|
committer | Dmitry Torokhov <dmitry.torokhov@gmail.com> | 2019-07-10 23:24:10 -0700 |
commit | 597473720f4dc69749542bfcfed4a927a43d935e (patch) | |
tree | 711bf773910fb93d1dd9120c633adc807685e0d8 /drivers/iio/light | |
parent | Input: atmel_mxt_ts - fix leak in mxt_update_cfg() (diff) | |
parent | Input: gpio_keys_polled - allow specifying name of input device (diff) | |
download | linux-dev-597473720f4dc69749542bfcfed4a927a43d935e.tar.xz linux-dev-597473720f4dc69749542bfcfed4a927a43d935e.zip |
Merge branch 'next' into for-linus
Prepare input updates for 5.3 merge window.
Diffstat (limited to 'drivers/iio/light')
-rw-r--r-- | drivers/iio/light/Kconfig | 23 | ||||
-rw-r--r-- | drivers/iio/light/Makefile | 2 | ||||
-rw-r--r-- | drivers/iio/light/isl29018.c | 46 | ||||
-rw-r--r-- | drivers/iio/light/max44009.c | 555 | ||||
-rw-r--r-- | drivers/iio/light/vcnl4035.c | 676 |
5 files changed, 1301 insertions, 1 deletions
diff --git a/drivers/iio/light/Kconfig b/drivers/iio/light/Kconfig index d66ea754ffff..5190eacfeb0a 100644 --- a/drivers/iio/light/Kconfig +++ b/drivers/iio/light/Kconfig @@ -299,6 +299,16 @@ config MAX44000 To compile this driver as a module, choose M here: the module will be called max44000. +config MAX44009 + tristate "MAX44009 Ambient Light Sensor" + depends on I2C + help + Say Y here if you want to build support for Maxim Integrated's + MAX44009 ambient light sensor device. + + To compile this driver as a module, choose M here: + the module will be called max44009. + config OPT3001 tristate "Texas Instruments OPT3001 Light Sensor" depends on I2C @@ -460,6 +470,19 @@ config VCNL4000 To compile this driver as a module, choose M here: the module will be called vcnl4000. +config VCNL4035 + tristate "VCNL4035 combined ALS and proximity sensor" + select IIO_TRIGGERED_BUFFER + select REGMAP_I2C + depends on I2C + help + Say Y here if you want to build a driver for the Vishay VCNL4035, + combined ambient light (ALS) and proximity sensor. Currently only ALS + function is available. + + To compile this driver as a module, choose M here: the + module will be called vcnl4035. + config VEML6070 tristate "VEML6070 UV A light sensor" depends on I2C diff --git a/drivers/iio/light/Makefile b/drivers/iio/light/Makefile index 86337b114bc4..e40794fbb435 100644 --- a/drivers/iio/light/Makefile +++ b/drivers/iio/light/Makefile @@ -28,6 +28,7 @@ obj-$(CONFIG_SENSORS_LM3533) += lm3533-als.o obj-$(CONFIG_LTR501) += ltr501.o obj-$(CONFIG_LV0104CS) += lv0104cs.o obj-$(CONFIG_MAX44000) += max44000.o +obj-$(CONFIG_MAX44009) += max44009.o obj-$(CONFIG_OPT3001) += opt3001.o obj-$(CONFIG_PA12203001) += pa12203001.o obj-$(CONFIG_RPR0521) += rpr0521.o @@ -45,6 +46,7 @@ obj-$(CONFIG_TSL2772) += tsl2772.o obj-$(CONFIG_TSL4531) += tsl4531.o obj-$(CONFIG_US5182D) += us5182d.o obj-$(CONFIG_VCNL4000) += vcnl4000.o +obj-$(CONFIG_VCNL4035) += vcnl4035.o obj-$(CONFIG_VEML6070) += veml6070.o obj-$(CONFIG_VL6180) += vl6180.o obj-$(CONFIG_ZOPT2201) += zopt2201.o diff --git a/drivers/iio/light/isl29018.c b/drivers/iio/light/isl29018.c index b45400f8fef4..846df4dce48c 100644 --- a/drivers/iio/light/isl29018.c +++ b/drivers/iio/light/isl29018.c @@ -23,6 +23,7 @@ #include <linux/mutex.h> #include <linux/delay.h> #include <linux/regmap.h> +#include <linux/regulator/consumer.h> #include <linux/slab.h> #include <linux/iio/iio.h> #include <linux/iio/sysfs.h> @@ -95,6 +96,7 @@ struct isl29018_chip { struct isl29018_scale scale; int prox_scheme; bool suspended; + struct regulator *vcc_reg; }; static int isl29018_set_integration_time(struct isl29018_chip *chip, @@ -708,6 +710,16 @@ static const char *isl29018_match_acpi_device(struct device *dev, int *data) return dev_name(dev); } +static void isl29018_disable_regulator_action(void *_data) +{ + struct isl29018_chip *chip = _data; + int err; + + err = regulator_disable(chip->vcc_reg); + if (err) + pr_err("failed to disable isl29018's VCC regulator!\n"); +} + static int isl29018_probe(struct i2c_client *client, const struct i2c_device_id *id) { @@ -742,6 +754,27 @@ static int isl29018_probe(struct i2c_client *client, chip->scale = isl29018_scales[chip->int_time][0]; chip->suspended = false; + chip->vcc_reg = devm_regulator_get(&client->dev, "vcc"); + if (IS_ERR(chip->vcc_reg)) { + err = PTR_ERR(chip->vcc_reg); + if (err != -EPROBE_DEFER) + dev_err(&client->dev, "failed to get VCC regulator!\n"); + return err; + } + + err = regulator_enable(chip->vcc_reg); + if (err) { + dev_err(&client->dev, "failed to enable VCC regulator!\n"); + return err; + } + + err = devm_add_action_or_reset(&client->dev, isl29018_disable_regulator_action, + chip); + if (err) { + dev_err(&client->dev, "failed to setup regulator cleanup action!\n"); + return err; + } + chip->regmap = devm_regmap_init_i2c(client, isl29018_chip_info_tbl[dev_id].regmap_cfg); if (IS_ERR(chip->regmap)) { @@ -768,6 +801,7 @@ static int isl29018_probe(struct i2c_client *client, static int isl29018_suspend(struct device *dev) { struct isl29018_chip *chip = iio_priv(dev_get_drvdata(dev)); + int ret; mutex_lock(&chip->lock); @@ -777,10 +811,13 @@ static int isl29018_suspend(struct device *dev) * So we do not have much to do here. */ chip->suspended = true; + ret = regulator_disable(chip->vcc_reg); + if (ret) + dev_err(dev, "failed to disable VCC regulator\n"); mutex_unlock(&chip->lock); - return 0; + return ret; } static int isl29018_resume(struct device *dev) @@ -790,6 +827,13 @@ static int isl29018_resume(struct device *dev) mutex_lock(&chip->lock); + err = regulator_enable(chip->vcc_reg); + if (err) { + dev_err(dev, "failed to enable VCC regulator\n"); + mutex_unlock(&chip->lock); + return err; + } + err = isl29018_chip_init(chip); if (!err) chip->suspended = false; diff --git a/drivers/iio/light/max44009.c b/drivers/iio/light/max44009.c new file mode 100644 index 000000000000..00ba15499638 --- /dev/null +++ b/drivers/iio/light/max44009.c @@ -0,0 +1,555 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * max44009.c - Support for MAX44009 Ambient Light Sensor + * + * Copyright (c) 2019 Robert Eshleman <bobbyeshleman@gmail.com> + * + * Datasheet: https://datasheets.maximintegrated.com/en/ds/MAX44009.pdf + * + * TODO: Support continuous mode and configuring from manual mode to + * automatic mode. + * + * Default I2C address: 0x4a + */ + +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/bits.h> +#include <linux/i2c.h> +#include <linux/iio/events.h> +#include <linux/iio/iio.h> +#include <linux/iio/sysfs.h> +#include <linux/interrupt.h> +#include <linux/module.h> +#include <linux/util_macros.h> + +#define MAX44009_DRV_NAME "max44009" + +/* Registers in datasheet order */ +#define MAX44009_REG_INT_STATUS 0x0 +#define MAX44009_REG_INT_EN 0x1 +#define MAX44009_REG_CFG 0x2 +#define MAX44009_REG_LUX_HI 0x3 +#define MAX44009_REG_LUX_LO 0x4 +#define MAX44009_REG_UPPER_THR 0x5 +#define MAX44009_REG_LOWER_THR 0x6 +#define MAX44009_REG_THR_TIMER 0x7 + +#define MAX44009_CFG_TIM_MASK GENMASK(2, 0) +#define MAX44009_CFG_MAN_MODE_MASK BIT(6) + +/* The maximum rising threshold for the max44009 */ +#define MAX44009_MAXIMUM_THRESHOLD 7520256 + +#define MAX44009_THRESH_EXP_MASK (0xf << 4) +#define MAX44009_THRESH_EXP_RSHIFT 4 +#define MAX44009_THRESH_MANT_LSHIFT 4 +#define MAX44009_THRESH_MANT_MASK 0xf + +#define MAX44009_UPPER_THR_MINIMUM 15 + +/* The max44009 always scales raw readings by 0.045 and is non-configurable */ +#define MAX44009_SCALE_NUMERATOR 45 +#define MAX44009_SCALE_DENOMINATOR 1000 + +/* The fixed-point fractional multiplier for de-scaling threshold values */ +#define MAX44009_FRACT_MULT 1000000 + +static const u32 max44009_int_time_ns_array[] = { + 800000000, + 400000000, + 200000000, + 100000000, + 50000000, /* Manual mode only */ + 25000000, /* Manual mode only */ + 12500000, /* Manual mode only */ + 6250000, /* Manual mode only */ +}; + +static const char max44009_int_time_str[] = + "0.8 " + "0.4 " + "0.2 " + "0.1 " + "0.05 " + "0.025 " + "0.0125 " + "0.00625"; + +struct max44009_data { + struct i2c_client *client; + struct mutex lock; +}; + +static const struct iio_event_spec max44009_event_spec[] = { + { + .type = IIO_EV_TYPE_THRESH, + .dir = IIO_EV_DIR_RISING, + .mask_separate = BIT(IIO_EV_INFO_VALUE) | + BIT(IIO_EV_INFO_ENABLE), + }, + { + .type = IIO_EV_TYPE_THRESH, + .dir = IIO_EV_DIR_FALLING, + .mask_separate = BIT(IIO_EV_INFO_VALUE) | + BIT(IIO_EV_INFO_ENABLE), + }, +}; + +static const struct iio_chan_spec max44009_channels[] = { + { + .type = IIO_LIGHT, + .info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED) | + BIT(IIO_CHAN_INFO_INT_TIME), + .event_spec = max44009_event_spec, + .num_event_specs = ARRAY_SIZE(max44009_event_spec), + }, +}; + +static int max44009_read_int_time(struct max44009_data *data) +{ + + int ret = i2c_smbus_read_byte_data(data->client, MAX44009_REG_CFG); + + if (ret < 0) + return ret; + + return max44009_int_time_ns_array[ret & MAX44009_CFG_TIM_MASK]; +} + +static int max44009_write_int_time(struct max44009_data *data, + int val, int val2) +{ + struct i2c_client *client = data->client; + int ret, int_time, config; + s64 ns; + + ns = val * NSEC_PER_SEC + val2; + int_time = find_closest_descending( + ns, + max44009_int_time_ns_array, + ARRAY_SIZE(max44009_int_time_ns_array)); + + ret = i2c_smbus_read_byte_data(client, MAX44009_REG_CFG); + if (ret < 0) + return ret; + + config = ret; + config &= int_time; + + /* + * To set the integration time, the device must also be in manual + * mode. + */ + config |= MAX44009_CFG_MAN_MODE_MASK; + + return i2c_smbus_write_byte_data(client, MAX44009_REG_CFG, config); +} + +static int max44009_write_raw(struct iio_dev *indio_dev, + struct iio_chan_spec const *chan, int val, + int val2, long mask) +{ + struct max44009_data *data = iio_priv(indio_dev); + int ret; + + if (mask == IIO_CHAN_INFO_INT_TIME && chan->type == IIO_LIGHT) { + mutex_lock(&data->lock); + ret = max44009_write_int_time(data, val, val2); + mutex_unlock(&data->lock); + return ret; + } + return -EINVAL; +} + +static int max44009_write_raw_get_fmt(struct iio_dev *indio_dev, + struct iio_chan_spec const *chan, + long mask) +{ + return IIO_VAL_INT_PLUS_NANO; +} + +static int max44009_lux_raw(u8 hi, u8 lo) +{ + int mantissa; + int exponent; + + /* + * The mantissa consists of the low nibble of the Lux High Byte + * and the low nibble of the Lux Low Byte. + */ + mantissa = ((hi & 0xf) << 4) | (lo & 0xf); + + /* The exponent byte is just the upper nibble of the Lux High Byte */ + exponent = (hi >> 4) & 0xf; + + /* + * The exponent value is base 2 to the power of the raw exponent byte. + */ + exponent = 1 << exponent; + + return exponent * mantissa; +} + +#define MAX44009_READ_LUX_XFER_LEN (4) + +static int max44009_read_lux_raw(struct max44009_data *data) +{ + int ret; + u8 hireg = MAX44009_REG_LUX_HI; + u8 loreg = MAX44009_REG_LUX_LO; + u8 lo = 0; + u8 hi = 0; + + struct i2c_msg msgs[] = { + { + .addr = data->client->addr, + .flags = 0, + .len = sizeof(hireg), + .buf = &hireg, + }, + { + .addr = data->client->addr, + .flags = I2C_M_RD, + .len = sizeof(hi), + .buf = &hi, + }, + { + .addr = data->client->addr, + .flags = 0, + .len = sizeof(loreg), + .buf = &loreg, + }, + { + .addr = data->client->addr, + .flags = I2C_M_RD, + .len = sizeof(lo), + .buf = &lo, + } + }; + + /* + * Use i2c_transfer instead of smbus read because i2c_transfer + * does NOT use a stop bit between address write and data read. + * Using a stop bit causes disjoint upper/lower byte reads and + * reduces accuracy. + */ + ret = i2c_transfer(data->client->adapter, + msgs, MAX44009_READ_LUX_XFER_LEN); + + if (ret != MAX44009_READ_LUX_XFER_LEN) + return -EIO; + + return max44009_lux_raw(hi, lo); +} + +static int max44009_read_raw(struct iio_dev *indio_dev, + struct iio_chan_spec const *chan, int *val, + int *val2, long mask) +{ + struct max44009_data *data = iio_priv(indio_dev); + int lux_raw; + int ret; + + switch (mask) { + case IIO_CHAN_INFO_PROCESSED: + switch (chan->type) { + case IIO_LIGHT: + ret = max44009_read_lux_raw(data); + if (ret < 0) + return ret; + lux_raw = ret; + + *val = lux_raw * MAX44009_SCALE_NUMERATOR; + *val2 = MAX44009_SCALE_DENOMINATOR; + return IIO_VAL_FRACTIONAL; + default: + return -EINVAL; + } + case IIO_CHAN_INFO_INT_TIME: + switch (chan->type) { + case IIO_LIGHT: + ret = max44009_read_int_time(data); + if (ret < 0) + return ret; + + *val2 = ret; + *val = 0; + return IIO_VAL_INT_PLUS_NANO; + default: + return -EINVAL; + } + default: + return -EINVAL; + } +} + +static IIO_CONST_ATTR(illuminance_integration_time_available, + max44009_int_time_str); + +static struct attribute *max44009_attributes[] = { + &iio_const_attr_illuminance_integration_time_available.dev_attr.attr, + NULL, +}; + +static const struct attribute_group max44009_attribute_group = { + .attrs = max44009_attributes, +}; + +static int max44009_threshold_byte_from_fraction(int integral, int fractional) +{ + int mantissa, exp; + + if ((integral <= 0 && fractional <= 0) || + integral > MAX44009_MAXIMUM_THRESHOLD || + (integral == MAX44009_MAXIMUM_THRESHOLD && fractional != 0)) + return -EINVAL; + + /* Reverse scaling of fixed-point integral */ + mantissa = integral * MAX44009_SCALE_DENOMINATOR; + mantissa /= MAX44009_SCALE_NUMERATOR; + + /* Reverse scaling of fixed-point fractional */ + mantissa += fractional / MAX44009_FRACT_MULT * + (MAX44009_SCALE_DENOMINATOR / MAX44009_SCALE_NUMERATOR); + + for (exp = 0; mantissa > 0xff; exp++) + mantissa >>= 1; + + mantissa >>= 4; + mantissa &= 0xf; + exp <<= 4; + + return exp | mantissa; +} + +static int max44009_get_thr_reg(enum iio_event_direction dir) +{ + switch (dir) { + case IIO_EV_DIR_RISING: + return MAX44009_REG_UPPER_THR; + case IIO_EV_DIR_FALLING: + return MAX44009_REG_LOWER_THR; + default: + return -EINVAL; + } +} + +static int max44009_write_event_value(struct iio_dev *indio_dev, + const struct iio_chan_spec *chan, + enum iio_event_type type, + enum iio_event_direction dir, + enum iio_event_info info, + int val, int val2) +{ + struct max44009_data *data = iio_priv(indio_dev); + int reg, threshold; + + if (info != IIO_EV_INFO_VALUE || chan->type != IIO_LIGHT) + return -EINVAL; + + threshold = max44009_threshold_byte_from_fraction(val, val2); + if (threshold < 0) + return threshold; + + reg = max44009_get_thr_reg(dir); + if (reg < 0) + return reg; + + return i2c_smbus_write_byte_data(data->client, reg, threshold); +} + +static int max44009_read_threshold(struct iio_dev *indio_dev, + enum iio_event_direction dir) +{ + struct max44009_data *data = iio_priv(indio_dev); + int byte, reg; + int mantissa, exponent; + + reg = max44009_get_thr_reg(dir); + if (reg < 0) + return reg; + + byte = i2c_smbus_read_byte_data(data->client, reg); + if (byte < 0) + return byte; + + mantissa = byte & MAX44009_THRESH_MANT_MASK; + mantissa <<= MAX44009_THRESH_MANT_LSHIFT; + + /* + * To get the upper threshold, always adds the minimum upper threshold + * value to the shifted byte value (see datasheet). + */ + if (dir == IIO_EV_DIR_RISING) + mantissa += MAX44009_UPPER_THR_MINIMUM; + + /* + * Exponent is base 2 to the power of the threshold exponent byte + * value + */ + exponent = byte & MAX44009_THRESH_EXP_MASK; + exponent >>= MAX44009_THRESH_EXP_RSHIFT; + + return (1 << exponent) * mantissa; +} + +static int max44009_read_event_value(struct iio_dev *indio_dev, + const struct iio_chan_spec *chan, + enum iio_event_type type, + enum iio_event_direction dir, + enum iio_event_info info, + int *val, int *val2) +{ + int ret; + int threshold; + + if (chan->type != IIO_LIGHT || type != IIO_EV_TYPE_THRESH) + return -EINVAL; + + ret = max44009_read_threshold(indio_dev, dir); + if (ret < 0) + return ret; + threshold = ret; + + *val = threshold * MAX44009_SCALE_NUMERATOR; + *val2 = MAX44009_SCALE_DENOMINATOR; + + return IIO_VAL_FRACTIONAL; +} + +static int max44009_write_event_config(struct iio_dev *indio_dev, + const struct iio_chan_spec *chan, + enum iio_event_type type, + enum iio_event_direction dir, + int state) +{ + struct max44009_data *data = iio_priv(indio_dev); + int ret; + + if (chan->type != IIO_LIGHT || type != IIO_EV_TYPE_THRESH) + return -EINVAL; + + ret = i2c_smbus_write_byte_data(data->client, + MAX44009_REG_INT_EN, state); + if (ret < 0) + return ret; + + /* + * Set device to trigger interrupt immediately upon exceeding + * the threshold limit. + */ + return i2c_smbus_write_byte_data(data->client, + MAX44009_REG_THR_TIMER, 0); +} + +static int max44009_read_event_config(struct iio_dev *indio_dev, + const struct iio_chan_spec *chan, + enum iio_event_type type, + enum iio_event_direction dir) +{ + struct max44009_data *data = iio_priv(indio_dev); + + if (chan->type != IIO_LIGHT || type != IIO_EV_TYPE_THRESH) + return -EINVAL; + + return i2c_smbus_read_byte_data(data->client, MAX44009_REG_INT_EN); +} + +static const struct iio_info max44009_info = { + .read_raw = max44009_read_raw, + .write_raw = max44009_write_raw, + .write_raw_get_fmt = max44009_write_raw_get_fmt, + .read_event_value = max44009_read_event_value, + .read_event_config = max44009_read_event_config, + .write_event_value = max44009_write_event_value, + .write_event_config = max44009_write_event_config, + .attrs = &max44009_attribute_group, +}; + +static irqreturn_t max44009_threaded_irq_handler(int irq, void *p) +{ + struct iio_dev *indio_dev = p; + struct max44009_data *data = iio_priv(indio_dev); + int ret; + + ret = i2c_smbus_read_byte_data(data->client, MAX44009_REG_INT_STATUS); + if (ret) { + iio_push_event(indio_dev, + IIO_UNMOD_EVENT_CODE(IIO_LIGHT, 0, + IIO_EV_TYPE_THRESH, + IIO_EV_DIR_EITHER), + iio_get_time_ns(indio_dev)); + + return IRQ_HANDLED; + } + + return IRQ_NONE; +} + +static int max44009_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct max44009_data *data; + struct iio_dev *indio_dev; + int ret; + + indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*data)); + if (!indio_dev) + return -ENOMEM; + + data = iio_priv(indio_dev); + i2c_set_clientdata(client, indio_dev); + data->client = client; + indio_dev->dev.parent = &client->dev; + indio_dev->info = &max44009_info; + indio_dev->modes = INDIO_DIRECT_MODE; + indio_dev->name = MAX44009_DRV_NAME; + indio_dev->channels = max44009_channels; + indio_dev->num_channels = ARRAY_SIZE(max44009_channels); + mutex_init(&data->lock); + + /* Clear any stale interrupt bit */ + ret = i2c_smbus_read_byte_data(client, MAX44009_REG_CFG); + if (ret < 0) + return ret; + + if (client->irq > 0) { + ret = devm_request_threaded_irq(&client->dev, client->irq, + NULL, + max44009_threaded_irq_handler, + IRQF_TRIGGER_FALLING | + IRQF_ONESHOT | IRQF_SHARED, + "max44009_event", + indio_dev); + if (ret < 0) + return ret; + } + + return devm_iio_device_register(&client->dev, indio_dev); +} + +static const struct i2c_device_id max44009_id[] = { + { "max44009", 0 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, max44009_id); + +static struct i2c_driver max44009_driver = { + .driver = { + .name = MAX44009_DRV_NAME, + }, + .probe = max44009_probe, + .id_table = max44009_id, +}; +module_i2c_driver(max44009_driver); + +static const struct of_device_id max44009_of_match[] = { + { .compatible = "maxim,max44009" }, + { } +}; +MODULE_DEVICE_TABLE(of, max44009_of_match); + +MODULE_AUTHOR("Robert Eshleman <bobbyeshleman@gmail.com>"); +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("MAX44009 ambient light sensor driver"); diff --git a/drivers/iio/light/vcnl4035.c b/drivers/iio/light/vcnl4035.c new file mode 100644 index 000000000000..cca4db312bd3 --- /dev/null +++ b/drivers/iio/light/vcnl4035.c @@ -0,0 +1,676 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * VCNL4035 Ambient Light and Proximity Sensor - 7-bit I2C slave address 0x60 + * + * Copyright (c) 2018, DENX Software Engineering GmbH + * Author: Parthiban Nallathambi <pn@denx.de> + * + * TODO: Proximity + */ +#include <linux/bitops.h> +#include <linux/i2c.h> +#include <linux/module.h> +#include <linux/pm_runtime.h> +#include <linux/regmap.h> + +#include <linux/iio/buffer.h> +#include <linux/iio/events.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> + +#define VCNL4035_DRV_NAME "vcnl4035" +#define VCNL4035_IRQ_NAME "vcnl4035_event" +#define VCNL4035_REGMAP_NAME "vcnl4035_regmap" + +/* Device registers */ +#define VCNL4035_ALS_CONF 0x00 +#define VCNL4035_ALS_THDH 0x01 +#define VCNL4035_ALS_THDL 0x02 +#define VCNL4035_ALS_DATA 0x0B +#define VCNL4035_WHITE_DATA 0x0C +#define VCNL4035_INT_FLAG 0x0D +#define VCNL4035_DEV_ID 0x0E + +/* Register masks */ +#define VCNL4035_MODE_ALS_MASK BIT(0) +#define VCNL4035_MODE_ALS_WHITE_CHAN BIT(8) +#define VCNL4035_MODE_ALS_INT_MASK BIT(1) +#define VCNL4035_ALS_IT_MASK GENMASK(7, 5) +#define VCNL4035_ALS_PERS_MASK GENMASK(3, 2) +#define VCNL4035_INT_ALS_IF_H_MASK BIT(12) +#define VCNL4035_INT_ALS_IF_L_MASK BIT(13) + +/* Default values */ +#define VCNL4035_MODE_ALS_ENABLE BIT(0) +#define VCNL4035_MODE_ALS_DISABLE 0x00 +#define VCNL4035_MODE_ALS_INT_ENABLE BIT(1) +#define VCNL4035_MODE_ALS_INT_DISABLE 0 +#define VCNL4035_DEV_ID_VAL 0x80 +#define VCNL4035_ALS_IT_DEFAULT 0x01 +#define VCNL4035_ALS_PERS_DEFAULT 0x00 +#define VCNL4035_ALS_THDH_DEFAULT 5000 +#define VCNL4035_ALS_THDL_DEFAULT 100 +#define VCNL4035_SLEEP_DELAY_MS 2000 + +struct vcnl4035_data { + struct i2c_client *client; + struct regmap *regmap; + unsigned int als_it_val; + unsigned int als_persistence; + unsigned int als_thresh_low; + unsigned int als_thresh_high; + struct iio_trigger *drdy_trigger0; +}; + +static inline bool vcnl4035_is_triggered(struct vcnl4035_data *data) +{ + int ret; + int reg; + + ret = regmap_read(data->regmap, VCNL4035_INT_FLAG, ®); + if (ret < 0) + return false; + + return !!(reg & + (VCNL4035_INT_ALS_IF_H_MASK | VCNL4035_INT_ALS_IF_L_MASK)); +} + +static irqreturn_t vcnl4035_drdy_irq_thread(int irq, void *private) +{ + struct iio_dev *indio_dev = private; + struct vcnl4035_data *data = iio_priv(indio_dev); + + if (vcnl4035_is_triggered(data)) { + iio_push_event(indio_dev, IIO_UNMOD_EVENT_CODE(IIO_LIGHT, + 0, + IIO_EV_TYPE_THRESH, + IIO_EV_DIR_EITHER), + iio_get_time_ns(indio_dev)); + iio_trigger_poll_chained(data->drdy_trigger0); + return IRQ_HANDLED; + } + + return IRQ_NONE; +} + +/* Triggered buffer */ +static irqreturn_t vcnl4035_trigger_consumer_handler(int irq, void *p) +{ + struct iio_poll_func *pf = p; + struct iio_dev *indio_dev = pf->indio_dev; + struct vcnl4035_data *data = iio_priv(indio_dev); + u8 buffer[ALIGN(sizeof(u16), sizeof(s64)) + sizeof(s64)]; + int ret; + + ret = regmap_read(data->regmap, VCNL4035_ALS_DATA, (int *)buffer); + if (ret < 0) { + dev_err(&data->client->dev, + "Trigger consumer can't read from sensor.\n"); + goto fail_read; + } + iio_push_to_buffers_with_timestamp(indio_dev, buffer, + iio_get_time_ns(indio_dev)); + +fail_read: + iio_trigger_notify_done(indio_dev->trig); + + return IRQ_HANDLED; +} + +static int vcnl4035_als_drdy_set_state(struct iio_trigger *trigger, + bool enable_drdy) +{ + struct iio_dev *indio_dev = iio_trigger_get_drvdata(trigger); + struct vcnl4035_data *data = iio_priv(indio_dev); + int val = enable_drdy ? VCNL4035_MODE_ALS_INT_ENABLE : + VCNL4035_MODE_ALS_INT_DISABLE; + + return regmap_update_bits(data->regmap, VCNL4035_ALS_CONF, + VCNL4035_MODE_ALS_INT_MASK, + val); +} + +static const struct iio_trigger_ops vcnl4035_trigger_ops = { + .validate_device = iio_trigger_validate_own_device, + .set_trigger_state = vcnl4035_als_drdy_set_state, +}; + +static int vcnl4035_set_pm_runtime_state(struct vcnl4035_data *data, bool on) +{ + int ret; + struct device *dev = &data->client->dev; + + if (on) { + ret = pm_runtime_get_sync(dev); + if (ret < 0) + pm_runtime_put_noidle(dev); + } else { + pm_runtime_mark_last_busy(dev); + ret = pm_runtime_put_autosuspend(dev); + } + + return ret; +} + +/* + * Device IT INT Time (ms) Scale (lux/step) + * 000 50 0.064 + * 001 100 0.032 + * 010 200 0.016 + * 100 400 0.008 + * 101 - 111 800 0.004 + * Values are proportional, so ALS INT is selected for input due to + * simplicity reason. Integration time value and scaling is + * calculated based on device INT value + * + * Raw value needs to be scaled using ALS steps + */ +static int vcnl4035_read_raw(struct iio_dev *indio_dev, + struct iio_chan_spec const *chan, int *val, + int *val2, long mask) +{ + struct vcnl4035_data *data = iio_priv(indio_dev); + int ret; + int raw_data; + unsigned int reg; + + switch (mask) { + case IIO_CHAN_INFO_RAW: + ret = vcnl4035_set_pm_runtime_state(data, true); + if (ret < 0) + return ret; + + ret = iio_device_claim_direct_mode(indio_dev); + if (!ret) { + if (chan->channel) + reg = VCNL4035_ALS_DATA; + else + reg = VCNL4035_WHITE_DATA; + ret = regmap_read(data->regmap, reg, &raw_data); + iio_device_release_direct_mode(indio_dev); + if (!ret) { + *val = raw_data; + ret = IIO_VAL_INT; + } + } + vcnl4035_set_pm_runtime_state(data, false); + return ret; + case IIO_CHAN_INFO_INT_TIME: + *val = 50; + if (data->als_it_val) + *val = data->als_it_val * 100; + return IIO_VAL_INT; + case IIO_CHAN_INFO_SCALE: + *val = 64; + if (!data->als_it_val) + *val2 = 1000; + else + *val2 = data->als_it_val * 2 * 1000; + return IIO_VAL_FRACTIONAL; + default: + return -EINVAL; + } +} + +static int vcnl4035_write_raw(struct iio_dev *indio_dev, + struct iio_chan_spec const *chan, + int val, int val2, long mask) +{ + int ret; + struct vcnl4035_data *data = iio_priv(indio_dev); + + switch (mask) { + case IIO_CHAN_INFO_INT_TIME: + if (val <= 0 || val > 800) + return -EINVAL; + + ret = vcnl4035_set_pm_runtime_state(data, true); + if (ret < 0) + return ret; + + ret = regmap_update_bits(data->regmap, VCNL4035_ALS_CONF, + VCNL4035_ALS_IT_MASK, + val / 100); + if (!ret) + data->als_it_val = val / 100; + + vcnl4035_set_pm_runtime_state(data, false); + return ret; + default: + return -EINVAL; + } +} + +/* No direct ABI for persistence and threshold, so eventing */ +static int vcnl4035_read_thresh(struct iio_dev *indio_dev, + const struct iio_chan_spec *chan, enum iio_event_type type, + enum iio_event_direction dir, enum iio_event_info info, + int *val, int *val2) +{ + struct vcnl4035_data *data = iio_priv(indio_dev); + + switch (info) { + case IIO_EV_INFO_VALUE: + switch (dir) { + case IIO_EV_DIR_RISING: + *val = data->als_thresh_high; + return IIO_VAL_INT; + case IIO_EV_DIR_FALLING: + *val = data->als_thresh_low; + return IIO_VAL_INT; + default: + return -EINVAL; + } + break; + case IIO_EV_INFO_PERIOD: + *val = data->als_persistence; + return IIO_VAL_INT; + default: + return -EINVAL; + } + +} + +static int vcnl4035_write_thresh(struct iio_dev *indio_dev, + const struct iio_chan_spec *chan, enum iio_event_type type, + enum iio_event_direction dir, enum iio_event_info info, int val, + int val2) +{ + struct vcnl4035_data *data = iio_priv(indio_dev); + int ret; + + switch (info) { + case IIO_EV_INFO_VALUE: + /* 16 bit threshold range 0 - 65535 */ + if (val < 0 || val > 65535) + return -EINVAL; + if (dir == IIO_EV_DIR_RISING) { + if (val < data->als_thresh_low) + return -EINVAL; + ret = regmap_write(data->regmap, VCNL4035_ALS_THDH, + val); + if (ret) + return ret; + data->als_thresh_high = val; + } else { + if (val > data->als_thresh_high) + return -EINVAL; + ret = regmap_write(data->regmap, VCNL4035_ALS_THDL, + val); + if (ret) + return ret; + data->als_thresh_low = val; + } + return ret; + case IIO_EV_INFO_PERIOD: + /* allow only 1 2 4 8 as persistence value */ + if (val < 0 || val > 8 || hweight8(val) != 1) + return -EINVAL; + ret = regmap_update_bits(data->regmap, VCNL4035_ALS_CONF, + VCNL4035_ALS_PERS_MASK, val); + if (!ret) + data->als_persistence = val; + return ret; + default: + return -EINVAL; + } +} + +static IIO_CONST_ATTR_INT_TIME_AVAIL("50 100 200 400 800"); + +static struct attribute *vcnl4035_attributes[] = { + &iio_const_attr_integration_time_available.dev_attr.attr, + NULL, +}; + +static const struct attribute_group vcnl4035_attribute_group = { + .attrs = vcnl4035_attributes, +}; + +static const struct iio_info vcnl4035_info = { + .read_raw = vcnl4035_read_raw, + .write_raw = vcnl4035_write_raw, + .read_event_value = vcnl4035_read_thresh, + .write_event_value = vcnl4035_write_thresh, + .attrs = &vcnl4035_attribute_group, +}; + +static const struct iio_event_spec vcnl4035_event_spec[] = { + { + .type = IIO_EV_TYPE_THRESH, + .dir = IIO_EV_DIR_RISING, + .mask_separate = BIT(IIO_EV_INFO_VALUE), + }, { + .type = IIO_EV_TYPE_THRESH, + .dir = IIO_EV_DIR_FALLING, + .mask_separate = BIT(IIO_EV_INFO_VALUE), + }, { + .type = IIO_EV_TYPE_THRESH, + .dir = IIO_EV_DIR_EITHER, + .mask_separate = BIT(IIO_EV_INFO_PERIOD), + }, +}; + +enum vcnl4035_scan_index_order { + VCNL4035_CHAN_INDEX_LIGHT, + VCNL4035_CHAN_INDEX_WHITE_LED, +}; + +static const struct iio_buffer_setup_ops iio_triggered_buffer_setup_ops = { + .validate_scan_mask = &iio_validate_scan_mask_onehot, +}; + +static const struct iio_chan_spec vcnl4035_channels[] = { + { + .type = IIO_LIGHT, + .channel = 0, + .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | + BIT(IIO_CHAN_INFO_INT_TIME) | + BIT(IIO_CHAN_INFO_SCALE), + .event_spec = vcnl4035_event_spec, + .num_event_specs = ARRAY_SIZE(vcnl4035_event_spec), + .scan_index = VCNL4035_CHAN_INDEX_LIGHT, + .scan_type = { + .sign = 'u', + .realbits = 16, + .storagebits = 16, + .endianness = IIO_LE, + }, + }, + { + .type = IIO_INTENSITY, + .channel = 1, + .modified = 1, + .channel2 = IIO_MOD_LIGHT_BOTH, + .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), + .scan_index = VCNL4035_CHAN_INDEX_WHITE_LED, + .scan_type = { + .sign = 'u', + .realbits = 16, + .storagebits = 16, + .endianness = IIO_LE, + }, + }, +}; + +static int vcnl4035_set_als_power_state(struct vcnl4035_data *data, u8 status) +{ + return regmap_update_bits(data->regmap, VCNL4035_ALS_CONF, + VCNL4035_MODE_ALS_MASK, + status); +} + +static int vcnl4035_init(struct vcnl4035_data *data) +{ + int ret; + int id; + + ret = regmap_read(data->regmap, VCNL4035_DEV_ID, &id); + if (ret < 0) { + dev_err(&data->client->dev, "Failed to read DEV_ID register\n"); + return ret; + } + + if (id != VCNL4035_DEV_ID_VAL) { + dev_err(&data->client->dev, "Wrong id, got %x, expected %x\n", + id, VCNL4035_DEV_ID_VAL); + return -ENODEV; + } + + ret = vcnl4035_set_als_power_state(data, VCNL4035_MODE_ALS_ENABLE); + if (ret < 0) + return ret; + + /* ALS white channel enable */ + ret = regmap_update_bits(data->regmap, VCNL4035_ALS_CONF, + VCNL4035_MODE_ALS_WHITE_CHAN, + 1); + if (ret) { + dev_err(&data->client->dev, "set white channel enable %d\n", + ret); + return ret; + } + + /* set default integration time - 100 ms for ALS */ + ret = regmap_update_bits(data->regmap, VCNL4035_ALS_CONF, + VCNL4035_ALS_IT_MASK, + VCNL4035_ALS_IT_DEFAULT); + if (ret) { + dev_err(&data->client->dev, "set default ALS IT returned %d\n", + ret); + return ret; + } + data->als_it_val = VCNL4035_ALS_IT_DEFAULT; + + /* set default persistence time - 1 for ALS */ + ret = regmap_update_bits(data->regmap, VCNL4035_ALS_CONF, + VCNL4035_ALS_PERS_MASK, + VCNL4035_ALS_PERS_DEFAULT); + if (ret) { + dev_err(&data->client->dev, "set default PERS returned %d\n", + ret); + return ret; + } + data->als_persistence = VCNL4035_ALS_PERS_DEFAULT; + + /* set default HIGH threshold for ALS */ + ret = regmap_write(data->regmap, VCNL4035_ALS_THDH, + VCNL4035_ALS_THDH_DEFAULT); + if (ret) { + dev_err(&data->client->dev, "set default THDH returned %d\n", + ret); + return ret; + } + data->als_thresh_high = VCNL4035_ALS_THDH_DEFAULT; + + /* set default LOW threshold for ALS */ + ret = regmap_write(data->regmap, VCNL4035_ALS_THDL, + VCNL4035_ALS_THDL_DEFAULT); + if (ret) { + dev_err(&data->client->dev, "set default THDL returned %d\n", + ret); + return ret; + } + data->als_thresh_low = VCNL4035_ALS_THDL_DEFAULT; + + return 0; +} + +static bool vcnl4035_is_volatile_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case VCNL4035_ALS_CONF: + case VCNL4035_DEV_ID: + return false; + default: + return true; + } +} + +static const struct regmap_config vcnl4035_regmap_config = { + .name = VCNL4035_REGMAP_NAME, + .reg_bits = 8, + .val_bits = 16, + .max_register = VCNL4035_DEV_ID, + .cache_type = REGCACHE_RBTREE, + .volatile_reg = vcnl4035_is_volatile_reg, + .val_format_endian = REGMAP_ENDIAN_LITTLE, +}; + +static int vcnl4035_probe_trigger(struct iio_dev *indio_dev) +{ + int ret; + struct vcnl4035_data *data = iio_priv(indio_dev); + + data->drdy_trigger0 = devm_iio_trigger_alloc( + indio_dev->dev.parent, + "%s-dev%d", indio_dev->name, indio_dev->id); + if (!data->drdy_trigger0) + return -ENOMEM; + + data->drdy_trigger0->dev.parent = indio_dev->dev.parent; + data->drdy_trigger0->ops = &vcnl4035_trigger_ops; + iio_trigger_set_drvdata(data->drdy_trigger0, indio_dev); + ret = devm_iio_trigger_register(indio_dev->dev.parent, + data->drdy_trigger0); + if (ret) { + dev_err(&data->client->dev, "iio trigger register failed\n"); + return ret; + } + + /* Trigger setup */ + ret = devm_iio_triggered_buffer_setup(indio_dev->dev.parent, indio_dev, + NULL, vcnl4035_trigger_consumer_handler, + &iio_triggered_buffer_setup_ops); + if (ret < 0) { + dev_err(&data->client->dev, "iio triggered buffer setup failed\n"); + return ret; + } + + /* IRQ to trigger mapping */ + ret = devm_request_threaded_irq(&data->client->dev, data->client->irq, + NULL, vcnl4035_drdy_irq_thread, + IRQF_TRIGGER_LOW | IRQF_ONESHOT, + VCNL4035_IRQ_NAME, indio_dev); + if (ret < 0) + dev_err(&data->client->dev, "request irq %d for trigger0 failed\n", + data->client->irq); + return ret; +} + +static int vcnl4035_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct vcnl4035_data *data; + struct iio_dev *indio_dev; + struct regmap *regmap; + int ret; + + indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*data)); + if (!indio_dev) + return -ENOMEM; + + regmap = devm_regmap_init_i2c(client, &vcnl4035_regmap_config); + if (IS_ERR(regmap)) { + dev_err(&client->dev, "regmap_init failed!\n"); + return PTR_ERR(regmap); + } + + data = iio_priv(indio_dev); + i2c_set_clientdata(client, indio_dev); + data->client = client; + data->regmap = regmap; + + indio_dev->dev.parent = &client->dev; + indio_dev->info = &vcnl4035_info; + indio_dev->name = VCNL4035_DRV_NAME; + indio_dev->channels = vcnl4035_channels; + indio_dev->num_channels = ARRAY_SIZE(vcnl4035_channels); + indio_dev->modes = INDIO_DIRECT_MODE; + + ret = vcnl4035_init(data); + if (ret < 0) { + dev_err(&client->dev, "vcnl4035 chip init failed\n"); + return ret; + } + + if (client->irq > 0) { + ret = vcnl4035_probe_trigger(indio_dev); + if (ret < 0) { + dev_err(&client->dev, "vcnl4035 unable init trigger\n"); + goto fail_poweroff; + } + } + + ret = pm_runtime_set_active(&client->dev); + if (ret < 0) + goto fail_poweroff; + + ret = iio_device_register(indio_dev); + if (ret < 0) + goto fail_poweroff; + + pm_runtime_enable(&client->dev); + pm_runtime_set_autosuspend_delay(&client->dev, VCNL4035_SLEEP_DELAY_MS); + pm_runtime_use_autosuspend(&client->dev); + + return 0; + +fail_poweroff: + vcnl4035_set_als_power_state(data, VCNL4035_MODE_ALS_DISABLE); + return ret; +} + +static int vcnl4035_remove(struct i2c_client *client) +{ + struct iio_dev *indio_dev = i2c_get_clientdata(client); + + pm_runtime_dont_use_autosuspend(&client->dev); + pm_runtime_disable(&client->dev); + iio_device_unregister(indio_dev); + pm_runtime_set_suspended(&client->dev); + + return vcnl4035_set_als_power_state(iio_priv(indio_dev), + VCNL4035_MODE_ALS_DISABLE); +} + +static int __maybe_unused vcnl4035_runtime_suspend(struct device *dev) +{ + struct iio_dev *indio_dev = i2c_get_clientdata(to_i2c_client(dev)); + struct vcnl4035_data *data = iio_priv(indio_dev); + int ret; + + ret = vcnl4035_set_als_power_state(data, VCNL4035_MODE_ALS_DISABLE); + regcache_mark_dirty(data->regmap); + + return ret; +} + +static int __maybe_unused vcnl4035_runtime_resume(struct device *dev) +{ + struct iio_dev *indio_dev = i2c_get_clientdata(to_i2c_client(dev)); + struct vcnl4035_data *data = iio_priv(indio_dev); + int ret; + + regcache_sync(data->regmap); + ret = vcnl4035_set_als_power_state(data, VCNL4035_MODE_ALS_ENABLE); + if (ret < 0) + return ret; + + /* wait for 1 ALS integration cycle */ + msleep(data->als_it_val * 100); + + return 0; +} + +static const struct dev_pm_ops vcnl4035_pm_ops = { + SET_SYSTEM_SLEEP_PM_OPS(pm_runtime_force_suspend, + pm_runtime_force_resume) + SET_RUNTIME_PM_OPS(vcnl4035_runtime_suspend, + vcnl4035_runtime_resume, NULL) +}; + +static const struct of_device_id vcnl4035_of_match[] = { + { .compatible = "vishay,vcnl4035", }, + { } +}; +MODULE_DEVICE_TABLE(of, vcnl4035_of_match); + +static struct i2c_driver vcnl4035_driver = { + .driver = { + .name = VCNL4035_DRV_NAME, + .pm = &vcnl4035_pm_ops, + .of_match_table = vcnl4035_of_match, + }, + .probe = vcnl4035_probe, + .remove = vcnl4035_remove, +}; + +module_i2c_driver(vcnl4035_driver); + +MODULE_AUTHOR("Parthiban Nallathambi <pn@denx.de>"); +MODULE_DESCRIPTION("VCNL4035 Ambient Light Sensor driver"); +MODULE_LICENSE("GPL v2"); |