[PATCH v2] misc: Add driver for GP2AP002 proximity/ambient light sensor

From: Donggeun Kim
Date: Fri Sep 09 2011 - 04:24:31 EST


SHARP GP2AP002 is proximity and ambient light sensor.
This patch supports it.

Signed-off-by: Donggeun Kim <dg77.kim@xxxxxxxxxxx>
Signed-off-by: Kyungmin Park <kyungmin.park@xxxxxxxxxxx>
---
Changes for v2
- changed to expose lux
- changed request_irq to request_threaded_irq function
- added sysfs_notify function call
- cleaned up code

Documentation/misc-devices/gp2ap002 | 44 +++
drivers/misc/Kconfig | 10 +
drivers/misc/Makefile | 1 +
drivers/misc/gp2ap002.c | 488 ++++++++++++++++++++++++++++++++
include/linux/platform_data/gp2ap002.h | 69 +++++
5 files changed, 612 insertions(+), 0 deletions(-)
create mode 100644 Documentation/misc-devices/gp2ap002
create mode 100644 drivers/misc/gp2ap002.c
create mode 100644 include/linux/platform_data/gp2ap002.h

diff --git a/Documentation/misc-devices/gp2ap002 b/Documentation/misc-devices/gp2ap002
new file mode 100644
index 0000000..3187740
--- /dev/null
+++ b/Documentation/misc-devices/gp2ap002
@@ -0,0 +1,44 @@
+Kernel driver gp2ap002
+=================
+
+Supported chips:
+* SHARP GP2AP002A00F proximity and ambient light sensor
+ Datasheet: Not publicly available
+
+Authors: Donggeun Kim <dg77.kim@xxxxxxxxxxx>
+ Minkyu Kang <mk7.kang@xxxxxxxxxxx>
+
+Description
+-----------
+GP2AP002A00F is a proximity and ambient light sensor.
+The proximity sensor operates in a normal or interrupt mode.
+1. Normal mode: read the register regarding proximity register
+2. Interrupt mode: the chip generates interrupts
+ whenever object is detected or not detected
+The proximity state can be obtained from VO field of PROX register
+ 1 = object is detected (value of VO field)
+ 0 = object is not detected (value of VO field)
+
+This chip only exports current as the result of ambient light sensor.
+To get illuminance, CPU measures the current exported
+from the sensor through ADC.
+The relationship between current and illuminance is as follows:
+ illuminance = 10^(current/10) - (1)
+This driver exposes illuminance
+by looking up the current-illuminance mapping table.
+
+Sysfs Interface
+---------------
+prox0_input proximity sensor result
+ 0: object is not detected
+ 1: object is detected
+ RO
+
+lux0_input ambient light sensor result
+ unit: lux
+ RO
+
+power_state enable/disable the sensor
+ 1: enable
+ 0: disable
+ RW
diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig
index 2d6423c..576e386 100644
--- a/drivers/misc/Kconfig
+++ b/drivers/misc/Kconfig
@@ -499,6 +499,16 @@ config USB_SWITCH_FSA9480
stereo and mono audio, video, microphone and UART data to use
a common connector port.

+config GP2AP002
+ tristate "SHARP GP2AP002 proximity/ambient light sensor"
+ depends on I2C
+ help
+ Say Y here if you want to support Sharp GP2AP002
+ proximity/ambient light sensor.
+
+ To compile this driver as a module, choose M here: the
+ module will be called GP2AP002.
+
source "drivers/misc/c2port/Kconfig"
source "drivers/misc/eeprom/Kconfig"
source "drivers/misc/cb710/Kconfig"
diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile
index 8f3efb6..7929c26 100644
--- a/drivers/misc/Makefile
+++ b/drivers/misc/Makefile
@@ -47,3 +47,4 @@ obj-$(CONFIG_AB8500_PWM) += ab8500-pwm.o
obj-y += lis3lv02d/
obj-y += carma/
obj-$(CONFIG_USB_SWITCH_FSA9480) += fsa9480.o
+obj-$(CONFIG_GP2AP002) += gp2ap002.o
diff --git a/drivers/misc/gp2ap002.c b/drivers/misc/gp2ap002.c
new file mode 100644
index 0000000..7ce5b37
--- /dev/null
+++ b/drivers/misc/gp2ap002.c
@@ -0,0 +1,488 @@
+/*
+ * gp2ap002.c - SHARP GP2AP002 proximity/ambient light sensor
+ *
+ * Copyright (C) 2010 Samsung Electronics
+ * Minkyu Kang <mk7.kang@xxxxxxxxxxx>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ */
+
+#include <linux/module.h>
+#include <linux/kernel.h>
+#include <linux/init.h>
+#include <linux/interrupt.h>
+#include <linux/platform_device.h>
+#include <linux/workqueue.h>
+#include <linux/mutex.h>
+#include <linux/err.h>
+#include <linux/i2c.h>
+#include <linux/slab.h>
+#include <linux/kobject.h>
+#include <linux/sysfs.h>
+
+#include <linux/platform_data/gp2ap002.h>
+
+#define GP2AP002_PROX 0x00
+#define GP2AP002_GAIN 0x01
+#define GP2AP002_HYS 0x02
+#define GP2AP002_CYCLE 0x03
+#define GP2AP002_OPMOD 0x04
+#define GP2AP002_CON 0x06
+
+#define PROX_VO_NO_DETECT (0 << 0)
+#define PROX_VO_DETECT (1 << 0)
+
+#define GAIN_LED_SHIFT (3)
+#define GAIN_LED_MASK (0x1 << GAIN_LED_SHIFT)
+
+#define HYS_HYSD_SHIFT (7)
+#define HYS_HYSD_MASK (0x1 << HYS_HYSD_SHIFT)
+#define HYS_HYSC_SHIFT (5)
+#define HYS_HYSC_MASK (0x3 << HYS_HYSC_SHIFT)
+#define HYS_HYSF_SHIFT (0)
+#define HYS_HYSF_MASK (0xf << HYS_HYSF_SHIFT)
+
+#define CYCLE_CYCL_SHIFT (3)
+#define CYCLE_CYCL_MASK (0x7 << CYCLE_CYCL_SHIFT)
+#define CYCLE_OSC_SHIFT (2)
+#define CYCLE_OSC_MASK (0x1 << CYCLE_OSC_SHIFT)
+
+#define OPMOD_ASD_SHIFT (4)
+#define OPMOD_ASD_MASK (0x1 << OPMOD_ASD_SHIFT)
+#define OPMOD_SSD_SHUTDOWN (0)
+#define OPMOD_SSD_OPERATING (1)
+#define OPMOD_VCON_SHIFT (1)
+#define OPMOD_VCON_MASK (0x1 << OPMOD_VCON_SHIFT)
+#define OPMOD_VCON_NORMAL (0 << 1)
+#define OPMOD_VCON_IRQ (1 << 1)
+
+#define CON_OCON_SHIFT (3)
+#define CON_OCON_MASK (0x3 << CON_OCON_SHIFT)
+
+struct illuminance {
+ unsigned int curr;
+ unsigned int lux;
+};
+
+/*
+ * This array maps current and lux.
+ * Ambient light sensing range is 3 to 55000.
+ * This mapping is based on the following formula.
+ * illuminance = 10 ^ (current / 10)
+ */
+const struct illuminance illuminance_table[] = {
+ { .curr = 5, .lux = 3 },
+ { .curr = 6, .lux = 4 },
+ { .curr = 7, .lux = 5 },
+ { .curr = 8, .lux = 6 },
+ { .curr = 9, .lux = 8 },
+ { .curr = 10, .lux = 10 },
+ { .curr = 11, .lux = 12 },
+ { .curr = 12, .lux = 16 },
+ { .curr = 13, .lux = 20 },
+ { .curr = 14, .lux = 25 },
+ { .curr = 15, .lux = 32 },
+ { .curr = 16, .lux = 40 },
+ { .curr = 17, .lux = 50 },
+ { .curr = 18, .lux = 63 },
+ { .curr = 19, .lux = 79 },
+ { .curr = 20, .lux = 100 },
+ { .curr = 21, .lux = 126 },
+ { .curr = 22, .lux = 158 },
+ { .curr = 23, .lux = 200 },
+ { .curr = 24, .lux = 251 },
+ { .curr = 25, .lux = 316 },
+ { .curr = 26, .lux = 398 },
+ { .curr = 27, .lux = 501 },
+ { .curr = 28, .lux = 631 },
+ { .curr = 29, .lux = 794 },
+ { .curr = 30, .lux = 1000 },
+ { .curr = 31, .lux = 1259 },
+ { .curr = 32, .lux = 1585 },
+ { .curr = 33, .lux = 1995 },
+ { .curr = 34, .lux = 2512 },
+ { .curr = 35, .lux = 3162 },
+ { .curr = 36, .lux = 3981 },
+ { .curr = 37, .lux = 5012 },
+ { .curr = 38, .lux = 6310 },
+ { .curr = 39, .lux = 7943 },
+ { .curr = 40, .lux = 10000 },
+ { .curr = 41, .lux = 12589 },
+ { .curr = 42, .lux = 15849 },
+ { .curr = 43, .lux = 19953 },
+ { .curr = 44, .lux = 25119 },
+ { .curr = 45, .lux = 31623 },
+ { .curr = 46, .lux = 39811 },
+ { .curr = 47, .lux = 50119 },
+};
+
+struct gp2ap002_chip {
+ struct i2c_client *client;
+ struct work_struct work;
+ struct mutex lock;
+
+ struct gp2ap002_platform_data *pdata;
+
+ bool enabled;
+
+ /* Proximity */
+ int proximity;
+
+ /* Ambient Light */
+ int adc;
+ int lux;
+};
+
+static void gp2ap002_get_proximity(struct gp2ap002_chip *chip)
+{
+ /* Determine whether the object is detected
+ by reading GP2AP002_PROX register */
+ chip->proximity = i2c_smbus_read_byte_data(chip->client,
+ GP2AP002_PROX) & PROX_VO_DETECT;
+}
+
+static int gp2ap002_chip_enable(struct gp2ap002_chip *chip, bool enable)
+{
+ struct gp2ap002_platform_data *pdata = chip->pdata;
+ int ret;
+ u8 value;
+
+ if (enable == chip->enabled)
+ return 0;
+
+ value = (pdata->analog_sleep << OPMOD_ASD_SHIFT) |
+ (pdata->prox_mode << OPMOD_VCON_SHIFT);
+ /* software shutdown mode */
+ if (enable)
+ value |= OPMOD_SSD_OPERATING;
+ else /* operating mode */
+ value |= OPMOD_SSD_SHUTDOWN;
+
+ ret = i2c_smbus_write_byte_data(chip->client, GP2AP002_OPMOD, value);
+ if (ret < 0)
+ goto out;
+
+ chip->enabled = enable;
+out:
+ return ret;
+}
+
+static void gp2ap002_work(struct work_struct *work)
+{
+ struct gp2ap002_chip *chip = container_of(work,
+ struct gp2ap002_chip, work);
+
+ mutex_lock(&chip->lock);
+ gp2ap002_get_proximity(chip);
+
+ kobject_uevent(&chip->client->dev.kobj, KOBJ_CHANGE);
+ sysfs_notify(&chip->client->dev.kobj, NULL, "prox0_input");
+ mutex_unlock(&chip->lock);
+
+ enable_irq(chip->client->irq);
+}
+
+static irqreturn_t gp2ap002_irq(int irq, void *data)
+{
+ struct gp2ap002_chip *chip = data;
+
+ disable_irq_nosync(irq);
+ schedule_work(&chip->work);
+
+ return IRQ_HANDLED;
+}
+
+static int lookup_lux(int curr)
+{
+ int start, end, mid = -1;
+ int table_size = ARRAY_SIZE(illuminance_table);
+
+ /* Do a binary search on illuminance table */
+ start = 0;
+ end = table_size;
+
+ while (end > start) {
+ mid = start + (end - start) / 2;
+ if (illuminance_table[mid].curr < curr)
+ end = mid;
+ else if (illuminance_table[mid].curr > curr)
+ start = mid + 1;
+ else
+ return mid;
+ }
+
+ return -ENODATA;
+}
+
+static ssize_t gp2ap002_show_proximity(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct gp2ap002_chip *chip = dev_get_drvdata(dev);
+
+ mutex_lock(&chip->lock);
+ gp2ap002_get_proximity(chip);
+ mutex_unlock(&chip->lock);
+
+ if (chip->proximity < 0)
+ return chip->proximity;
+ return sprintf(buf, "%d\n", chip->proximity);
+}
+static DEVICE_ATTR(prox0_input, S_IRUGO, gp2ap002_show_proximity, NULL);
+
+static ssize_t gp2ap002_show_lux(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct gp2ap002_chip *chip = dev_get_drvdata(dev);
+ struct gp2ap002_platform_data *pdata = chip->pdata;
+ int ret;
+
+ if (pdata->get_adc) {
+ mutex_lock(&chip->lock);
+ chip->adc = pdata->get_adc();
+ ret = lookup_lux(chip->adc);
+ if (ret < 0) {
+ mutex_unlock(&chip->lock);
+ return ret;
+ }
+ chip->lux = illuminance_table[ret].lux;
+ mutex_unlock(&chip->lock);
+ }
+ return sprintf(buf, "%d\n", chip->lux);
+}
+static DEVICE_ATTR(lux0_input, S_IRUGO, gp2ap002_show_lux, NULL);
+
+static ssize_t gp2ap002_store_enable(struct device *dev,
+ struct device_attribute *attr, const char *buf, size_t count)
+{
+ struct gp2ap002_chip *chip = dev_get_drvdata(dev);
+ unsigned long value;
+ int ret;
+
+ if (strict_strtoul(buf, 0, &value))
+ return -EINVAL;
+
+ ret = gp2ap002_chip_enable(chip, !!value);
+ if (ret < 0)
+ return ret;
+
+ return count;
+}
+static ssize_t gp2ap002_show_enable(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct gp2ap002_chip *chip = dev_get_drvdata(dev);
+
+ return sprintf(buf, "%d\n", chip->enabled);
+}
+static DEVICE_ATTR(power_state, S_IRUGO | S_IWUSR,
+ gp2ap002_show_enable, gp2ap002_store_enable);
+
+static struct attribute *gp2ap002_attributes[] = {
+ &dev_attr_prox0_input.attr,
+ &dev_attr_lux0_input.attr,
+ &dev_attr_power_state.attr,
+ NULL
+};
+
+static const struct attribute_group gp2ap002_attribute_group = {
+ .attrs = gp2ap002_attributes,
+};
+
+static int gp2ap002_initialize(struct gp2ap002_chip *chip)
+{
+ struct gp2ap002_platform_data *pdata = chip->pdata;
+ int ret;
+ u8 value;
+
+ /* GAIN register */
+ value = (pdata->led_mode << GAIN_LED_SHIFT) & GAIN_LED_MASK;
+ ret = i2c_smbus_write_byte_data(chip->client, GP2AP002_GAIN, value);
+ if (ret < 0)
+ goto out;
+
+ /* HYS register */
+ value = (pdata->hysd << HYS_HYSD_SHIFT) & HYS_HYSD_MASK;
+ value |= (pdata->hysc << HYS_HYSC_SHIFT) & HYS_HYSC_MASK;
+ value |= (pdata->hysf << HYS_HYSF_SHIFT) & HYS_HYSF_MASK;
+ ret = i2c_smbus_write_byte_data(chip->client, GP2AP002_HYS, value);
+ if (ret < 0)
+ goto out;
+
+ /* CYCLE register */
+ value = (pdata->cycle << CYCLE_CYCL_SHIFT) & CYCLE_CYCL_MASK;
+ value |= (pdata->oscillator << CYCLE_OSC_SHIFT) & CYCLE_OSC_MASK;
+ ret = i2c_smbus_write_byte_data(chip->client, GP2AP002_CYCLE, value);
+ if (ret < 0)
+ goto out;
+
+ /* CON register */
+ value = (pdata->vout_control << CON_OCON_SHIFT) & CON_OCON_MASK;
+ ret = i2c_smbus_write_byte_data(chip->client, GP2AP002_CYCLE, value);
+out:
+ return ret;
+}
+
+static int __devinit gp2ap002_probe(struct i2c_client *client,
+ const struct i2c_device_id *id)
+{
+ struct gp2ap002_chip *chip;
+ struct gp2ap002_platform_data *pdata;
+ int ret;
+
+ pdata = client->dev.platform_data;
+ if (!pdata) {
+ dev_err(&client->dev, "No platform init data supplied\n");
+ return -ENODEV;
+ }
+
+ chip = kzalloc(sizeof(struct gp2ap002_chip), GFP_KERNEL);
+ if (!chip) {
+ dev_err(&client->dev, "Failed to allocate driver structure\n");
+ return -ENOMEM;
+ }
+
+ if (client->irq > 0) {
+ ret = request_threaded_irq(client->irq, NULL, gp2ap002_irq,
+ IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
+ "GP2AP002 sensor", chip);
+ if (ret) {
+ dev_err(&client->dev, "Failed to request irq: %d\n",
+ client->irq);
+ goto error_irq;
+ }
+ }
+
+ chip->client = client;
+ i2c_set_clientdata(client, chip);
+ INIT_WORK(&chip->work, gp2ap002_work);
+ mutex_init(&chip->lock);
+ chip->pdata = pdata;
+
+ ret = sysfs_create_group(&client->dev.kobj, &gp2ap002_attribute_group);
+ if (ret) {
+ dev_err(&client->dev,
+ "Failed to create attribute group\n");
+ goto error_sysfs_group;
+ }
+
+ if (pdata->power_enable)
+ pdata->power_enable(true);
+
+ ret = gp2ap002_initialize(chip);
+ if (ret) {
+ dev_err(&client->dev, "Failed to initialize chip\n");
+ goto error_chip_initialize;
+ }
+
+ ret = gp2ap002_chip_enable(chip, pdata->chip_enable);
+ if (ret) {
+ dev_err(&client->dev, "Failed to enable chip\n");
+ goto error_chip_enable;
+ }
+
+ dev_info(&client->dev, "%s registered\n", id->name);
+ return 0;
+
+error_chip_enable:
+error_chip_initialize:
+ sysfs_remove_group(&client->dev.kobj, &gp2ap002_attribute_group);
+error_sysfs_group:
+ if (client->irq > 0)
+ free_irq(client->irq, chip);
+error_irq:
+ kfree(chip);
+ return ret;
+}
+
+static int __devexit gp2ap002_remove(struct i2c_client *client)
+{
+ struct gp2ap002_chip *chip = i2c_get_clientdata(client);
+
+ disable_irq_nosync(client->irq);
+ cancel_work_sync(&chip->work);
+
+ if (client->irq > 0)
+ free_irq(client->irq, chip);
+
+ sysfs_remove_group(&client->dev.kobj, &gp2ap002_attribute_group);
+
+ gp2ap002_chip_enable(chip, false);
+
+ if (chip->pdata->power_enable)
+ chip->pdata->power_enable(false);
+
+ kfree(chip);
+
+ return 0;
+}
+
+#ifdef CONFIG_PM
+static int gp2ap002_suspend(struct device *dev)
+{
+ struct i2c_client *client = container_of(dev, struct i2c_client, dev);
+ struct gp2ap002_chip *chip = i2c_get_clientdata(client);
+
+ cancel_work_sync(&chip->work);
+ gp2ap002_chip_enable(chip, false);
+
+ if (chip->pdata->power_enable)
+ chip->pdata->power_enable(false);
+
+ return 0;
+}
+
+static int gp2ap002_resume(struct device *dev)
+{
+ struct i2c_client *client = container_of(dev, struct i2c_client, dev);
+ struct gp2ap002_chip *chip = i2c_get_clientdata(client);
+
+ if (chip->pdata->power_enable)
+ chip->pdata->power_enable(true);
+
+ gp2ap002_chip_enable(chip, true);
+
+ return 0;
+}
+#else
+#define gp2ap002_suspend NULL
+#define gp2ap002_resume NULL
+#endif
+
+static const struct i2c_device_id gp2ap002_id[] = {
+ { "GP2AP002", 0 },
+ { }
+};
+MODULE_DEVICE_TABLE(i2c, gp2ap002_id);
+
+static const struct dev_pm_ops gp2ap002_pm_ops = {
+ SET_SYSTEM_SLEEP_PM_OPS(gp2ap002_suspend, gp2ap002_resume)
+};
+
+static struct i2c_driver gp2ap002_i2c_driver = {
+ .driver = {
+ .name = "GP2AP002",
+ .owner = THIS_MODULE,
+ .pm = &gp2ap002_pm_ops,
+ },
+ .probe = gp2ap002_probe,
+ .remove = __devexit_p(gp2ap002_remove),
+ .id_table = gp2ap002_id,
+};
+
+static int __init gp2ap002_init(void)
+{
+ return i2c_add_driver(&gp2ap002_i2c_driver);
+}
+module_init(gp2ap002_init);
+
+static void __exit gp2ap002_exit(void)
+{
+ i2c_del_driver(&gp2ap002_i2c_driver);
+}
+module_exit(gp2ap002_exit);
+
+MODULE_AUTHOR("Minkyu Kang <mk7.kang@xxxxxxxxxxx>");
+MODULE_DESCRIPTION("GP2AP002 Proximity/Ambient Light Sensor driver");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/platform_data/gp2ap002.h b/include/linux/platform_data/gp2ap002.h
new file mode 100644
index 0000000..39e6a1b
--- /dev/null
+++ b/include/linux/platform_data/gp2ap002.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2010 Samsung Electronics
+ * Minkyu Kang <mk7.kang@xxxxxxxxxxx>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ */
+
+#ifndef __GP2AP002_H_
+#define __GP2AP002_H_
+
+/**
+ * struct gp2ap002_platform_data
+ * @get_adc: call back function to get adc value for ambient light sensor
+ * @power_enable: call back function to control voltage supply for chip
+ * @led_mode: select switch for LED's resistance
+ * 0: 2x higher setting,
+ * 1: normal setting
+ * @hysd, hysc, hysf: a set of these bits adjusts the sensitivity,
+ * determining characteristics of the detection distance and
+ * its hysteresis
+ * 0 <= hysd <= 1
+ * 0 <= hysc <= 3
+ * 0 <= hysf <= 15
+ * @cycle: determine the detection cycle
+ * 0: 8ms (response time)
+ * 1: 16ms (response time)
+ * 2: 32ms (response time)
+ * 3: 64ms (response time)
+ * 4: 128ms (response time)
+ * 5: 256ms (response time)
+ * 6: 512ms (response time)
+ * 7: 1024ms (response time)
+ * @oscillator: select switch for internal clock frequency hopping
+ * 0: effective,
+ * 1: ineffective
+ * @analog_sleep: select switch for analog sleep function
+ * 0: ineffective
+ * 1: effective
+ * @prox_mode: determine output method control for Vout pin
+ * 0: normal mode
+ * 1: interrupt_mode
+ * @vout_control: select switch for enabling/disabling Vout pin
+ * 0: enable
+ * 2: force to go Low
+ * 3: force to go High
+ * @chip_enable: determine enabling/disabling software shutdown function
+ * 0: shutdown mode
+ * 1: operating mode
+ */
+struct gp2ap002_platform_data {
+ int (*get_adc)(void);
+ void (*power_enable)(bool);
+
+ u8 led_mode;
+ u8 hysd;
+ u8 hysc;
+ u8 hysf;
+ u8 cycle;
+ u8 oscillator;
+ u8 analog_sleep;
+ u8 prox_mode;
+ u8 vout_control;
+
+ bool chip_enable;
+};
+
+#endif
--
1.7.4.1

--
To unsubscribe from this list: send the line "unsubscribe linux-kernel" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at http://vger.kernel.org/majordomo-info.html
Please read the FAQ at http://www.tux.org/lkml/