Added IIO driver for Avago's APDS 9930 proximity and ALS sensor. The current implementation is for ALS functionality. It offers ACPI/DT support, irq and event handling, raw readings. Datasheet for this device can be found at http://www.avagotech.com/docs/AV02-3190EN. Signed-off-by: Cristina Ciocan <cristina.ciocan@xxxxxxxxx> --- drivers/iio/light/Kconfig | 10 + drivers/iio/light/Makefile | 1 + drivers/iio/light/apds9930.c | 912 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 923 insertions(+) create mode 100644 drivers/iio/light/apds9930.c diff --git a/drivers/iio/light/Kconfig b/drivers/iio/light/Kconfig index cd937d9..abeca21 100644 --- a/drivers/iio/light/Kconfig +++ b/drivers/iio/light/Kconfig @@ -37,6 +37,16 @@ config APDS9300 To compile this driver as a module, choose M here: the module will be called apds9300. +config APDS9930 + tristate "APDS9930 ambient light sensor" + depends on I2C + help + Say Y here if you want to build a driver for the Avago APDS9930 + ambient light sensor. + + To compile this driver as a module, choose M here: the + module will be called apds9930. + config CM32181 depends on I2C tristate "CM32181 driver" diff --git a/drivers/iio/light/Makefile b/drivers/iio/light/Makefile index ad7c30f..535d289 100644 --- a/drivers/iio/light/Makefile +++ b/drivers/iio/light/Makefile @@ -6,6 +6,7 @@ obj-$(CONFIG_ADJD_S311) += adjd_s311.o obj-$(CONFIG_AL3320A) += al3320a.o obj-$(CONFIG_APDS9300) += apds9300.o +obj-$(CONFIG_APDS9930) += apds9930.o obj-$(CONFIG_CM32181) += cm32181.o obj-$(CONFIG_CM3232) += cm3232.o obj-$(CONFIG_CM3323) += cm3323.o diff --git a/drivers/iio/light/apds9930.c b/drivers/iio/light/apds9930.c new file mode 100644 index 0000000..47b53d1 --- /dev/null +++ b/drivers/iio/light/apds9930.c @@ -0,0 +1,912 @@ +/* + * This is a driver for Avago APDS 9930 ALS sensor chip. It + * is inspired from drivers/misc/apds990x.c to use IIO. + * + * The I2C slave address for this device is 0x39. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + */ + +#include <linux/module.h> +#include <linux/i2c.h> +#include <linux/pm.h> +#include <linux/mutex.h> +#include <linux/kernel.h> +#include <linux/mutex.h> +#include <linux/interrupt.h> +#include <linux/acpi.h> +#include <linux/delay.h> +#include <linux/property.h> + +#include <linux/iio/types.h> +#include <linux/iio/iio.h> +#include <linux/iio/events.h> + +#include <linux/gpio/consumer.h> + +#define APDS9930_DRIVER_NAME "apds9930" +#define APDS9930_IRQ_NAME "apds9930_irq" +#define APDS9930_GPIO_NAME "apds9930_gpio" + +#define APDS9930_INIT_SLEEP 5 /* sleep for 5 ms before issuing commands */ + +/* I2C command register - fields and possible field values */ +/* CMD bit value must be set to 1 when addressing command register */ +#define APDS9930_CMD_SELECT 0x80 +/* TYPE possible values */ +#define APDS9930_CMD_TYPE_RB 0 /* Repeated byte */ +#define APDS9930_CMD_TYPE_AUTO_INC BIT(5) /* Auto-increment */ +#define APDS9930_CMD_TYPE_SPECIAL_FUNC GENMASK(6, 5) /* Special function */ +/* ADDR possible values */ +#define APDS9930_CMD_TYPE_ALS 0x6 /* ALS interrupt clear */ +/* Clear masks */ +#define APDS9930_CLEAR_CMD_TYPE_MASK GENMASK(4, 0) +/* Shortcut to clear and set CMD and TYPE fields */ +#define CMD_REG_SETUP(reg, transaction_type) { \ + reg &= APDS9930_CLEAR_CMD_TYPE_MASK; \ + reg |= APDS9930_CMD_SELECT | transaction_type; \ +} + +/* Register set (rw = read/write, r = read, w = write) */ +#define APDS9930_ENABLE_REG 0x00 /* rw-Enable of states and interrupts */ +#define APDS9930_ATIME_REG 0x01 /* rw-ALS ADC time*/ +#define APDS9930_WTIME_REG 0x03 /* rw-Wait time */ +#define APDS9930_AILTL_REG 0x04 /* rw-ALS interrupt low threshold low + * byte + */ +#define APDS9930_AIHTL_REG 0x06 /* rw-ALS interrupt high threshold low + * byte + */ +#define APDS9930_PERS_REG 0x0C /* rw-Interrupt persistence filters */ +#define APDS9930_CONFIG_REG 0x0D /* rw-Configuration */ +#define APDS9930_CONTROL_REG 0x0F /* rw-Gain control register */ +#define APDS9930_ID_REG 0x12 /* r-Device ID */ +#define APDS9930_STATUS_REG 0x13 /* r-Device status */ +#define APDS9930_CDATAL_REG 0x14 /* r-Ch0 ADC low data register */ +#define APDS9930_IRDATAL_REG 0x16 /* r-Ch1 ADC low data register */ + +/* Useful bits per register */ + +/* Enable register */ +#define APDS9930_AIEN BIT(4) /* ALS interrupt enable */ +/* Persistence register */ +#define APDS9930_APERS_SHIFT 0 /* Interrupt persistence */ +/* Configuration register */ +#define APDS9930_AGL_SHIFT 2 /* ALS gain level */ +/* Control register */ +#define APDS9930_AGAIN_SHIFT 0 /* ALS gain control */ +/* Device ID - possible values */ +#define APDS9930_ID 0x39 +/* Status register */ +#define APDS9930_AINT BIT(4) /* ALS interrupt */ + +/* Default values (already shifted) for registers content */ +#define APDS9930_DISABLE_ALL 0 /* Disable and powerdown */ +#define APDS9930_ENABLE_ALL 0x13 /* Set all ALS bits and power on */ +#define APDS9930_DEF_ATIME 0xdb /* 101 ms */ +#define APDS9930_DEF_WTIME 0xff /* 2.7 ms - min wait time */ +#define APDS9930_DEF_AGAIN 2 /* 16 x ALS gain */ +#define APDS9930_DEF_APERS 0 /* Each over/underflow triggers an + * interrupt + */ + +/* + * Interrupt threshold defaults - setting low to maximum will trigger a first + * interrupt to allow us to read the data registers status and properly set a + * threshold value to match the current environment. + * + * The default high threshold is set only for consistency, since the registers + * are checked against in order: first if the value of CDATA is above LOW; if + * so, trigger interrupt and do not check the HIGH threshold. + */ +#define APDS9930_DEF_ALS_THRESH_LOW 0xffff +#define APDS9930_DEF_ALS_THRESH_HIGH 0x0 + +/* Default, open air, coefficients (for Lux computation) */ +#define DEF_DF 52 /* Device factor */ +/* + * Material-depending coefficients (set to open air values). They are scaled for + * computation purposes by 100. + */ +#define DEF_LUX_DIVISION_SCALE 10000 +#define DEF_GA 49 +#define DEF_B 186 +#define DEF_C 74 +#define DEF_D 129 + +/* Possible ALS gain values (with AGL cleared) */ +static const int again_values[] = {1, 8, 16, 120}; +#define APDS9930_MAX_AGAIN_INDEX 3 +#define APDS9930_AGL_DIVISION_SCALE 6 + +/* + * Percentages from the maximum CH0 value that indicate the recorded CH0 data is + * too low or too high - for AGAIN adapting purposes. + */ +#define APDS9930_CH0_HIGH_PERCENTAGE 90 +#define APDS9930_CH0_LOW_PERCENTAGE 10 + +/* + * With how much (in percentages) must the CH0 value differ from one step to + * another in order to consider a significant change in light and trigger an + * interrupt. + */ +#define APDS9930_ALS_HYSTERESIS 20 + +/* + * Number of consecutive out-of-range measurements needed to trigger a gain + * change. We count them and not change the gain each time the CH0 data is + * out-of-range because it takes a couple of cycles for system to adapt to new + * gain values. + */ +#define APDS9930_MAX_COUNTS_FOR_GAIN_CHANGE 2 + +/* DT or ACPI device properties names */ +#define GA_PROP "intel,ga" +#define COEF_B_PROP "intel,coeff-B" +#define COEF_C_PROP "intel,coeff-B" +#define COEF_D_PROP "intel,coeff-D" +#define DF_PROP "intel,df" +#define AGAIN_PROP "intel,als-gain" +#define ATIME_PROP "intel,atime" + +struct apds9930_coefficients { + /* GA, B, C and D coefficients are scaled by 100 for computational + purposes. */ + int ga; + int coef_a; /* This is 1, but needs to be set to a scaled value, thus if + we decide to change the scale, this coefficient must also + be changed. */ + int coef_b; + int coef_c; + int coef_d; + int df; +}; + +struct apds9930_device_data { + struct apds9930_coefficients coefs; + + u8 again; /* AGAIN value (index in again_values[]) */ + u8 atime; /* ALS integration time */ +}; + +struct apds9930_threshold { + u16 low; + u16 high; +}; + +struct apds9930_data { + struct i2c_client *client; + struct mutex mutex; /* Protect access to device data */ + + struct apds9930_device_data device_data; + +#define __coefs device_data.coefs +#define __again device_data.again +#define __atime device_data.atime + + u8 alsit; + u16 ch0_max; /* Maximum possible Ch0 data value */ + bool agl_enabled; + int again_change; /* Number of consecutive situations in + * which the gain needed to be changed + */ + bool als_intr_state; + struct apds9930_threshold als_thresh; +}; + +/* + * Writes data to the register; the next i2c_write call will write to the same + * register. + */ +static int apds9930_write_byte(struct i2c_client *client, u8 reg, u8 data) +{ + CMD_REG_SETUP(reg, APDS9930_CMD_TYPE_RB); + + return i2c_smbus_write_byte_data(client, reg, data); +} + +/* + * Reads data from a register; the next i2c_read call will read from the same + * address. + */ +static int apds9930_read_byte(struct i2c_client *client, u8 reg, u8 *data) +{ + int ret; + + CMD_REG_SETUP(reg, APDS9930_CMD_TYPE_RB); + ret = i2c_smbus_read_byte_data(client, reg); + *data = ret; + + return ret; +} + +/* Writes 2 bytes at the given address */ +static int apds9930_write_word(struct i2c_client *client, u8 reg, u16 data) +{ + CMD_REG_SETUP(reg, APDS9930_CMD_TYPE_AUTO_INC); + + return i2c_smbus_write_word_data(client, reg, data); +} + +/* Reads 2 bytes from the given address */ +static int apds9930_read_word(struct i2c_client *client, u8 reg, u16 *data) +{ + int ret; + + CMD_REG_SETUP(reg, APDS9930_CMD_TYPE_AUTO_INC); + ret = i2c_smbus_read_word_data(client, reg); + *data = ret; + + return ret; +} + +/* ALSIT = 2.73ms * (256 - ATIME), 2.73 = 5591/(2^11) */ +static inline u8 atime_to_alsit(u8 atime_val) +{ + return (u8)(((256 - (u32)atime_val) * 5591) >> 11); +} + +/* ATIME = 256 - ALSIT/2.73ms, 1/2.73 = 375/(2^10) */ +static inline u8 alsit_to_atime(u8 alsit_val) +{ + return (u8)(256 - (((u32)alsit_val * 375) >> 10)); +} + +/* Computes maximum Ch0 data when atime is the given one. */ +static inline u16 apds9930_compute_max_ch0(u8 atime_val) +{ + return 1024 * (256 - (u32)atime_val) - 1; +} + +/* Computes the ALS gain value for the next step and updates the current one */ +static void apds9930_update_again(struct apds9930_data *data, u16 ch0) +{ + int current_index, next_index, err; + + /* Compute the value for the next measurement */ + current_index = data->__again; + next_index = data->__again; + + /* CH0 data too high, try to lower the ALS gain if possible */ + if (ch0 >= (data->ch0_max * APDS9930_CH0_HIGH_PERCENTAGE) / 100) { + if (data->again_change + 1 < + APDS9930_MAX_COUNTS_FOR_GAIN_CHANGE) + data->again_change++; + else { + data->again_change = 0; + + if (next_index == 0 && !(data->agl_enabled)) { + err = apds9930_write_byte( + data->client, + APDS9930_CONFIG_REG, + 1 << APDS9930_AGL_SHIFT); + if (!err) + data->agl_enabled = true; + } else if (next_index > 0) { + next_index--; + } + } + } + + /* CH0 data too low, try to increase the ALS gain if possible */ + else if (ch0 <= (data->ch0_max * APDS9930_CH0_LOW_PERCENTAGE) / 100) { + if (data->again_change + 1 < + APDS9930_MAX_COUNTS_FOR_GAIN_CHANGE) + data->again_change++; + else { + data->again_change = 0; + if (data->agl_enabled) { + err = apds9930_write_byte(data->client, + APDS9930_CONFIG_REG, + 0); + if (!err) + data->agl_enabled = false; + } else if (next_index < APDS9930_MAX_AGAIN_INDEX) { + next_index++; + } + } + } + + if (next_index != current_index) { + /* Update data's index value */ + data->__again = next_index; + + /* Update AGAIN for the next reading */ + apds9930_write_byte(data->client, APDS9930_CONTROL_REG, + data->__again << APDS9930_AGAIN_SHIFT); + } +} + +/* + * Update thresholds so as to generate interrupts when the CH0 data changes + * significantly since the last update; significantly is quantifies by the + * hysteresis factor: if the ALS state changes with more than hysteresis %, + * update the thresholds. + */ +static void apds9930_update_als_thresholds(struct apds9930_data *data, u16 ch0) +{ + data->als_thresh.low = (ch0*(100 - APDS9930_ALS_HYSTERESIS))/100; + data->als_thresh.high = min((ch0*(100 + APDS9930_ALS_HYSTERESIS))/100, + (int)data->ch0_max); + + apds9930_write_word(data->client, APDS9930_AILTL_REG, + data->als_thresh.low); + apds9930_write_word(data->client, APDS9930_AIHTL_REG, + data->als_thresh.high); +} + +/* Having the channel 0 and channels 1's data, compute the Lux value */ +static int apds9930_compute_lux(struct apds9930_data *data, u16 ch0, u16 ch1) +{ + long int iac1, iac2, alsit_val, again_val, tmp_iac; + unsigned long int iac, lux; + struct apds9930_coefficients cf; + + /* Lux equation */ + cf = data->__coefs; + iac1 = cf.coef_a * ch0 - cf.coef_b * ch1; + iac2 = cf.coef_c * ch0 - cf.coef_d * ch1; + tmp_iac = max(iac1, iac2); + iac = (tmp_iac < 0) ? 0 : (unsigned long)tmp_iac; + alsit_val = (int)(data->alsit); + again_val = again_values[data->__again]; + lux = (iac * cf.ga * cf.df) / (alsit_val * again_val * + DEF_LUX_DIVISION_SCALE); + + return data->agl_enabled ? (APDS9930_AGL_DIVISION_SCALE * lux) : lux; +} + +static int apds9930_enable_all(struct apds9930_data *data) +{ + int ret; + + mutex_lock(&data->mutex); + ret = apds9930_write_byte(data->client, APDS9930_ENABLE_REG, + APDS9930_ENABLE_ALL); + if (ret < 0) + goto err; + + data->als_intr_state = true; +err: + mutex_unlock(&data->mutex); + + return ret; +} + +static int apds9930_disable_all(struct apds9930_data *data) +{ + int ret; + + mutex_lock(&data->mutex); + ret = apds9930_write_byte(data->client, APDS9930_ENABLE_REG, + APDS9930_DISABLE_ALL); + if (ret < 0) + goto err; + + data->als_intr_state = false; +err: + mutex_unlock(&data->mutex); + + return ret; +} + +static int apds9930_chip_detect(struct apds9930_data *data) +{ + int ret; + u8 id; + + ret = apds9930_read_byte(data->client, APDS9930_ID_REG, &id); + if (ret < 0) + return ret; + + if (id != APDS9930_ID) + ret = -EINVAL; + + return ret; +} + +/* + * Set the coefficients to those specified in properties if they exist, + * otherwise to default values. + */ +static void apds9930_set_device_data(struct apds9930_data *data) +{ + struct apds9930_coefficients defaults = { + .ga = DEF_GA, + .coef_a = 100, + .coef_b = DEF_B, + .coef_c = DEF_C, + .coef_d = DEF_D, + .df = DEF_DF, + }; + struct device *dev = &data->client->dev; + + /* + * Look for device properties and set them to property value or to + * default. + */ + if (device_property_read_u32(dev, GA_PROP, &data->__coefs.ga) != 0) + data->__coefs.ga = defaults.ga; + + if (device_property_read_u32(dev, DF_PROP, &data->__coefs.df) != 0) + data->__coefs.df = defaults.df; + + if (device_property_read_u32(dev, COEF_B_PROP, + &data->__coefs.coef_b) != 0 || + device_property_read_u32(dev, COEF_C_PROP, + &data->__coefs.coef_c) != 0 || + device_property_read_u32(dev, COEF_D_PROP, + &data->__coefs.coef_d) != 0) { + data->__coefs.coef_b = defaults.coef_b; + data->__coefs.coef_c = defaults.coef_c; + data->__coefs.coef_d = defaults.coef_d; + } + data->__coefs.coef_a = defaults.coef_a; + + if (device_property_read_u32(dev, ATIME_PROP, + (int *)&data->__atime) != 0) + data->__atime = APDS9930_DEF_ATIME; + + if (device_property_read_u32(dev, AGAIN_PROP, + (int *)&data->__again) != 0) + data->__again = APDS9930_DEF_AGAIN; + + /* + * AGAIN can only have the following values (as found in the register): + * 0, 1, 2, 3. Check if we receive invalid data and fall back to + * defaults if that is the case. + */ + if (data->__again > APDS9930_MAX_AGAIN_INDEX) + data->__again = APDS9930_DEF_AGAIN; +} + +static void apds9930_chip_data_init(struct apds9930_data *data) +{ + apds9930_set_device_data(data); + + /* Init data to default values */ + data->alsit = atime_to_alsit(data->__atime); + data->ch0_max = apds9930_compute_max_ch0(APDS9930_DEF_ATIME); + data->agl_enabled = false; + data->again_change = 0; + data->als_intr_state = false; + data->als_thresh.low = APDS9930_DEF_ALS_THRESH_LOW; + data->als_thresh.high = APDS9930_DEF_ALS_THRESH_HIGH; +} + +/* Basic chip initialization, as described in the datasheet */ +static int apds9930_chip_registers_init(struct apds9930_data *data) +{ + struct i2c_client *client = data->client; + int ret; + + /* Disable and powerdown device */ + ret = apds9930_disable_all(data); + if (ret < 0) + return ret; + + /* Set timing registers default values (minimum) */ + ret = apds9930_write_byte(client, APDS9930_ATIME_REG, + APDS9930_DEF_ATIME); + if (ret < 0) + return ret; + ret = apds9930_write_byte(client, APDS9930_WTIME_REG, + APDS9930_DEF_WTIME); + if (ret < 0) + return ret; + + /* Reset the configuration register (do not wait long) */ + ret = apds9930_write_byte(client, APDS9930_CONFIG_REG, 0); + if (ret < 0) + return ret; + + /* Gain selection setting */ + ret = apds9930_write_byte(client, APDS9930_CONTROL_REG, + data->__again << APDS9930_AGAIN_SHIFT); + if (ret < 0) + return ret; + + /* Interrupt threshold default settings */ + ret = apds9930_write_word(client, APDS9930_AILTL_REG, + APDS9930_DEF_ALS_THRESH_LOW); + if (ret < 0) + return ret; + ret = apds9930_write_word(client, APDS9930_AIHTL_REG, + APDS9930_DEF_ALS_THRESH_HIGH); + + /* Set ALS persistence filter to default values */ + ret = apds9930_write_byte(client, APDS9930_PERS_REG, + APDS9930_DEF_APERS << APDS9930_APERS_SHIFT); + if (ret < 0) + return ret; + + /* Power the device back on */ + return apds9930_enable_all(data); +} + +static int apds9930_read_raw(struct iio_dev *indio_dev, + struct iio_chan_spec const *chan, int *val, + int *val2, long mask) +{ + struct apds9930_data *data = iio_priv(indio_dev); + struct i2c_client *client = data->client; + int ret; + u16 ch0, ch1, ch; + + mutex_lock(&data->mutex); + switch (chan->type) { + case IIO_LIGHT: + /* Get Lux value */ + ret = apds9930_read_word(client, APDS9930_CDATAL_REG, + &ch0); + if (ret < 0) + break; + ret = apds9930_read_word(client, APDS9930_IRDATAL_REG, + &ch1); + if (ret < 0) + break; + + /* Compute Lux value */ + *val = apds9930_compute_lux(data, ch0, ch1); + apds9930_update_again(data, ch0); + ret = IIO_VAL_INT; + break; + case IIO_INTENSITY: + /* Get ch0 or ch1 raw value */ + ret = apds9930_read_word(client, chan->channel ? + APDS9930_IRDATAL_REG : + APDS9930_CDATAL_REG, + &ch); + if (ret < 0) + break; + + *val = (int)ch; + ret = IIO_VAL_INT; + break; + default: + ret = -EINVAL; + break; + } + mutex_unlock(&data->mutex); + + return ret; +} + +/* Event handling functions */ +static int apds9930_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 apds9930_data *data = iio_priv(indio_dev); + + switch (chan->type) { + case IIO_INTENSITY: + return data->als_intr_state; + default: + return -EINVAL; + } +} + +static int apds9930_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 apds9930_data *data = iio_priv(indio_dev); + struct i2c_client *client = data->client; + u8 enable_reg; + int ret; + + if (chan->type != IIO_INTENSITY) + return -EINVAL; + + mutex_lock(&data->mutex); + + ret = apds9930_read_byte(client, APDS9930_ENABLE_REG, &enable_reg); + if (ret < 0) + goto err; + + if (state) + enable_reg |= APDS9930_AIEN; /* enable */ + else + enable_reg &= ~APDS9930_AIEN; /* disable */ + + ret = apds9930_write_byte(client, APDS9930_ENABLE_REG, enable_reg); + if (ret == 0) + data->als_intr_state = (bool)state; +err: + mutex_unlock(&data->mutex); + + return ret; +} + +static int apds9930_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) +{ + struct apds9930_data *data = iio_priv(indio_dev); + struct apds9930_threshold thresh; + + switch (chan->type) { + case IIO_INTENSITY: + thresh = data->als_thresh; + break; + default: + return -EINVAL; + } + + switch (dir) { + case IIO_EV_DIR_RISING: + *val = (int)(thresh.high); + break; + case IIO_EV_DIR_FALLING: + *val = (int)(thresh.low); + break; + default: + return -EINVAL; + } + + return IIO_VAL_INT; +} + +/* IIO device specific data structures */ +static const struct iio_info apds9930_info = { + .driver_module = THIS_MODULE, + .read_raw = apds9930_read_raw, + .read_event_config = apds9930_read_event_config, + .write_event_config = apds9930_write_event_config, + .read_event_value = apds9930_read_event_value, +}; + +static const struct iio_event_spec apds9930_als_event_spec[] = { + { + /* ALS Ch0 interrupt "above threshold" event */ + .type = IIO_EV_TYPE_THRESH, + .dir = IIO_EV_DIR_RISING, + .mask_separate = BIT(IIO_EV_INFO_VALUE) | + BIT(IIO_EV_INFO_ENABLE), + }, { + /* ALS Ch0 interrupt "below threshold" event */ + .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 apds9930_channels[] = { + /* ALS channels */ + { + /* Lux (processed Ch0 + Ch1) */ + .type = IIO_LIGHT, + .channel = 0, + .info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED), + }, { + /* + * Ch0 photodiode (visible light + infrared); threshold + * triggered event + */ + .type = IIO_INTENSITY, + .channel = 0, + .modified = true, + .channel2 = IIO_MOD_LIGHT_BOTH, + .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), + .event_spec = apds9930_als_event_spec, + .num_event_specs = ARRAY_SIZE(apds9930_als_event_spec), + }, { + /* Ch1 photodiode (infrared) */ + .type = IIO_INTENSITY, + .channel = 1, + .modified = true, + .channel2 = IIO_MOD_LIGHT_IR, + .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), + } +}; + +/* Determine what has triggered the interrupt and clear it accordingly */ +static int apds9930_interrupt_clear(struct apds9930_data *data, u8 intr_status) +{ + u8 reg = 0x00; + + if (intr_status & APDS9930_AINT) { + CMD_REG_SETUP(reg, APDS9930_CMD_TYPE_SPECIAL_FUNC); + reg |= APDS9930_CMD_TYPE_ALS; + + return i2c_smbus_read_byte_data(data->client, reg); + } + + return -EINVAL; +} + +/* ALS interrupt handler */ +static irqreturn_t apds9930_irq_handler(int irq, void *private_data) +{ + struct iio_dev *indio_dev = private_data; + struct apds9930_data *data = iio_priv(indio_dev); + u8 status, enable_reg; + u16 ch0; + int ret; + + /* Disable ALS function while processing data */ + ret = apds9930_read_byte(data->client, APDS9930_ENABLE_REG, + &enable_reg); + if (ret < 0) + return IRQ_HANDLED; + ret = apds9930_write_byte(data->client, APDS9930_ENABLE_REG, 1); + if (ret < 0) + return IRQ_HANDLED; + + /* Read status register to see what caused the interrupt */ + ret = apds9930_read_byte(data->client, APDS9930_STATUS_REG, &status); + if (ret < 0) + goto err; + + ret = apds9930_read_word(data->client, APDS9930_CDATAL_REG, &ch0); + if (ret < 0) + goto err; + + apds9930_update_again(data, ch0); + + /* Push event to userspace */ + if (status & APDS9930_AINT) { + /* Clear interrupt */ + apds9930_interrupt_clear(data, status); + + iio_push_event(indio_dev, + IIO_MOD_EVENT_CODE(IIO_INTENSITY, + 0, + IIO_MOD_LIGHT_BOTH, + IIO_EV_TYPE_THRESH, + IIO_EV_DIR_EITHER), + iio_get_time_ns()); + + /* Update ALS thresholds to environment */ + apds9930_update_als_thresholds(data, ch0); + } + +err: + /* Re-enable ALS converter */ + apds9930_write_byte(data->client, APDS9930_ENABLE_REG, enable_reg); + + return IRQ_HANDLED; +} + +static int apds9930_gpio_probe(struct i2c_client *client, + struct apds9930_data *data) +{ + struct device *dev; + struct gpio_desc *gpio; + int ret; + + if (!client) + return -EINVAL; + + dev = &client->dev; + + gpio = devm_gpiod_get_index(dev, APDS9930_GPIO_NAME, 0); + if (IS_ERR(gpio)) { + dev_err(dev, "ACPI GPIO get index failed\n"); + return PTR_ERR(gpio); + } + + ret = gpiod_direction_input(gpio); + if (ret) + return ret; + + return gpiod_to_irq(gpio); +} + +static int apds9930_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct apds9930_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; + + mutex_init(&data->mutex); + + /* Recommended wait time after power on. */ + msleep(APDS9930_INIT_SLEEP); + + /* Check if the chip is the one we are expecting */ + ret = apds9930_chip_detect(data); + if (ret < 0) + goto err; + + apds9930_chip_data_init(data); + ret = apds9930_chip_registers_init(data); + if (ret < 0) + goto err; + + indio_dev->dev.parent = &client->dev; + indio_dev->name = APDS9930_DRIVER_NAME; + indio_dev->modes = INDIO_DIRECT_MODE; + indio_dev->channels = apds9930_channels; + indio_dev->num_channels = ARRAY_SIZE(apds9930_channels); + indio_dev->info = &apds9930_info; + + if (client->irq <= 0) + client->irq = apds9930_gpio_probe(client, data); + + if (client->irq > 0) { + ret = devm_request_threaded_irq(&client->dev, + client->irq, + NULL, + apds9930_irq_handler, + IRQF_TRIGGER_FALLING | + IRQF_ONESHOT, + APDS9930_IRQ_NAME, + indio_dev); + if (ret < 0) + goto err; + } + + ret = iio_device_register(indio_dev); + if (ret < 0) + goto err; + + return 0; +err: + apds9930_disable_all(data); + + return ret; +} + +static int apds9930_remove(struct i2c_client *client) +{ + struct iio_dev *indio_dev = i2c_get_clientdata(client); + struct apds9930_data *data = iio_priv(indio_dev); + int ret; + + /* Disable interrupts */ + ret = apds9930_disable_all(data); + + iio_device_unregister(indio_dev); + + return ret; +} + +static const struct acpi_device_id apds9930_acpi_table[] = { + {"APDS9930", 0}, + {} +}; +MODULE_DEVICE_TABLE(acpi, apds9930_acpi_table); + +static const struct i2c_device_id apds9930_ids_table[] = { + {"apds9930", 0}, + {} +}; +MODULE_DEVICE_TABLE(i2c, apds9930_ids_table); + +static struct i2c_driver apds9930_iio_driver = { + .driver = { + .owner = THIS_MODULE, + .name = APDS9930_DRIVER_NAME, + .acpi_match_table = ACPI_PTR(apds9930_acpi_table), + }, + .probe = apds9930_probe, + .remove = apds9930_remove, + .id_table = apds9930_ids_table, +}; +module_i2c_driver(apds9930_iio_driver); + +MODULE_AUTHOR("Cristina Ciocan <cristina.ciocan@xxxxxxxxx>"); +MODULE_DESCRIPTION("APDS-9930 ALS sensor IIO driver"); +MODULE_LICENSE("GPL v2"); -- 1.8.1.2 -- 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