[PATCH v3 1/2] iio: light: add driver for bh1730fvc chips

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



Ambient light sensor that supports visible light and IR measurements and
configurable gain/integration time.

This is written as an additional driver instead of being added into the
existing bh1750 / bh1780 drivers. The bh1730 chip is significantly
different from either of these two:

* bh1750 is not fully smbus compatible and only partially supports
  continuous reading mode since not all of its supported chips have this
  feature.

* bh1780 does gain adjustment in hardware and exposes lux values
  directly, as opposed to bh1730 which provides two raw channels (IR +
  visible light) and requires manual gain adjustment and lux computation
  in the driver.

Changed in v2:
* Split off DT documentation change into a separate commit.
* Use i2c's probe_new.

Changed in v3:
* Moved from 64 bit to 32 bit arithmetic where possible.
* Changed IIO channel configuration for consistency with other drivers.
* Refactored mathematical computations for readability.
* Removed unnecessary CONFIG_OF checks.

Signed-off-by: Pierre Bourdon <delroth@xxxxxxxxxx>
---
 drivers/iio/light/Kconfig  |  10 +
 drivers/iio/light/Makefile |   1 +
 drivers/iio/light/bh1730.c | 434 +++++++++++++++++++++++++++++++++++++
 3 files changed, 445 insertions(+)
 create mode 100644 drivers/iio/light/bh1730.c

diff --git a/drivers/iio/light/Kconfig b/drivers/iio/light/Kconfig
index 93fd421b10d7..286a7f78762b 100644
--- a/drivers/iio/light/Kconfig
+++ b/drivers/iio/light/Kconfig
@@ -63,6 +63,16 @@ config APDS9960
 	  To compile this driver as a module, choose M here: the
 	  module will be called apds9960
 
+config BH1730
+	tristate "ROHM BH1730 ambient light sensor"
+	depends on I2C
+	help
+	 Say Y here to build support for the ROHM BH1730FVC ambient
+	 light sensor.
+
+	 To compile this driver as a module, choose M here: the module will
+	 be called bh1730.
+
 config BH1750
 	tristate "ROHM BH1750 ambient light sensor"
 	depends on I2C
diff --git a/drivers/iio/light/Makefile b/drivers/iio/light/Makefile
index f714067a7816..eb9010a10536 100644
--- a/drivers/iio/light/Makefile
+++ b/drivers/iio/light/Makefile
@@ -9,6 +9,7 @@ obj-$(CONFIG_ADJD_S311)		+= adjd_s311.o
 obj-$(CONFIG_AL3320A)		+= al3320a.o
 obj-$(CONFIG_APDS9300)		+= apds9300.o
 obj-$(CONFIG_APDS9960)		+= apds9960.o
+obj-$(CONFIG_BH1730)		+= bh1730.o
 obj-$(CONFIG_BH1750)		+= bh1750.o
 obj-$(CONFIG_BH1780)		+= bh1780.o
 obj-$(CONFIG_CM32181)		+= cm32181.o
diff --git a/drivers/iio/light/bh1730.c b/drivers/iio/light/bh1730.c
new file mode 100644
index 000000000000..4a23fbd6ff84
--- /dev/null
+++ b/drivers/iio/light/bh1730.c
@@ -0,0 +1,434 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * ROHM BH1730 ambient light sensor driver
+ *
+ * Copyright (c) 2018 Google, Inc.
+ * Author: Pierre Bourdon <delroth@xxxxxxxxxx>
+ *
+ * Based on a previous non-iio BH1730FVC driver:
+ * Copyright (C) 2012 Samsung Electronics. All rights reserved.
+ * Author: Won Huh <won.huh@xxxxxxxxxxx>
+ *
+ * Data sheets:
+ *  http://www.rohm.com/web/global/datasheet/BH1730FVC/bh1730fvc-e
+ */
+
+#include <linux/delay.h>
+#include <linux/i2c.h>
+#include <linux/iio/iio.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/time.h>
+
+#define BH1730_CMD_BIT BIT(7)
+
+#define BH1730_REG_CONTROL	0x00
+#define BH1730_REG_TIMING	0x01
+#define BH1730_REG_INTERRUPT	0x02
+#define BH1730_REG_THLLOW	0x03
+#define BH1730_REG_THLHIGH	0x04
+#define BH1730_REG_THHLOW	0x05
+#define BH1730_REG_THHHIGH	0x06
+#define BH1730_REG_GAIN		0x07
+#define BH1730_REG_ID		0x12
+#define BH1730_REG_DATA0LOW	0x14
+#define BH1730_REG_DATA0HIGH	0x15
+#define BH1730_REG_DATA1LOW	0x16
+#define BH1730_REG_DATA1HIGH	0x17
+
+#define BH1730_CONTROL_POWER_ON		BIT(0)
+#define BH1730_CONTROL_MEASURE		BIT(1)
+
+#define BH1730_INTERNAL_CLOCK_NS	2800
+
+#define BH1730_DEFAULT_INTEG_MS		150
+
+enum bh1730_gain {
+	BH1730_GAIN_1X = 0,
+	BH1730_GAIN_2X,
+	BH1730_GAIN_64X,
+	BH1730_GAIN_128X,
+};
+#define BH1730_MAX_GAIN_MULTIPLIER 128
+
+struct bh1730_data {
+	struct i2c_client *client;
+	enum bh1730_gain gain;
+	int itime;
+};
+#define BH1730_MAX_GAIN_MULTIPLIER 128
+
+static int bh1730_read_word(struct bh1730_data *bh1730, u8 reg)
+{
+	int ret = i2c_smbus_read_word_data(bh1730->client,
+					   BH1730_CMD_BIT | reg);
+	if (ret < 0)
+		dev_err(&bh1730->client->dev,
+			"i2c read failed error %d, register %01x\n",
+			ret, reg);
+
+	return ret;
+}
+
+static int bh1730_write(struct bh1730_data *bh1730, u8 reg, u8 val)
+{
+	int ret = i2c_smbus_write_byte_data(bh1730->client,
+					    BH1730_CMD_BIT | reg,
+					    val);
+	if (ret < 0)
+		dev_err(&bh1730->client->dev,
+			"i2c write failed error %d, register %01x\n",
+			ret, reg);
+
+	return ret;
+}
+
+static int gain_setting_to_multiplier(enum bh1730_gain gain)
+{
+	switch (gain) {
+	case BH1730_GAIN_1X:
+		return 1;
+	case BH1730_GAIN_2X:
+		return 2;
+	case BH1730_GAIN_64X:
+		return 64;
+	case BH1730_GAIN_128X:
+		return 128;
+	default:
+		return -EINVAL;
+	}
+}
+
+static int bh1730_gain_multiplier(struct bh1730_data *bh1730)
+{
+	int multiplier = gain_setting_to_multiplier(bh1730->gain);
+
+	if (multiplier < 0) {
+		dev_warn(&bh1730->client->dev,
+			 "invalid gain multiplier settings: %d\n",
+			 bh1730->gain);
+		bh1730->gain = BH1730_GAIN_1X;
+		multiplier = 1;
+	}
+
+	return multiplier;
+}
+
+static int bh1730_itime_us(struct bh1730_data *bh1730)
+{
+	return (BH1730_INTERNAL_CLOCK_NS * 964 * (256 - bh1730->itime))
+			/ NSEC_PER_USEC;
+}
+
+static int bh1730_set_gain(struct bh1730_data *bh1730, enum bh1730_gain gain)
+{
+	int ret = bh1730_write(bh1730, BH1730_REG_GAIN, gain);
+
+	if (ret < 0)
+		return ret;
+
+	bh1730->gain = gain;
+
+	return 0;
+}
+
+static int bh1730_set_integration_time_ms(struct bh1730_data *bh1730,
+					  int time_ms)
+{
+	int ret, time_ns, itime;
+
+	/* Prefilter obviously invalid time_ms values that would overflow. */
+	if (time_ms <= 0 || time_ms > 1000)
+		goto out_of_range;
+
+	time_ns = time_ms * NSEC_PER_MSEC;
+	itime = 256 - DIV_ROUND_CLOSEST(time_ns,
+					BH1730_INTERNAL_CLOCK_NS * 964);
+
+	/* ITIME == 0 is reserved for manual integration mode. */
+	if (itime <= 0 || itime > 255)
+		goto out_of_range;
+
+	ret = bh1730_write(bh1730, BH1730_REG_TIMING, itime);
+	if (ret < 0)
+		return ret;
+
+	bh1730->itime = itime;
+
+	return 0;
+
+out_of_range:
+	dev_warn(&bh1730->client->dev, "integration time out of range: %dms\n",
+		 time_ms);
+
+	return -ERANGE;
+}
+
+static void bh1730_wait_for_next_measurement(struct bh1730_data *bh1730)
+{
+	udelay(bh1730_itime_us(bh1730) +
+		DIV_ROUND_UP(BH1730_INTERNAL_CLOCK_NS * 714, NSEC_PER_USEC));
+}
+
+static int bh1730_adjust_gain(struct bh1730_data *bh1730)
+{
+	int visible, ir, highest, gain, ret, i;
+
+	visible = bh1730_read_word(bh1730, BH1730_REG_DATA0LOW);
+	if (visible < 0)
+		return visible;
+
+	ir = bh1730_read_word(bh1730, BH1730_REG_DATA1LOW);
+	if (ir < 0)
+		return ir;
+
+	highest = max(visible, ir);
+
+	/*
+	 * If the read value is being clamped, assume the worst and go to the
+	 * lowest possible gain. The alternative is doing multiple
+	 * recalibrations, which would be slower and have the same effect.
+	 */
+	if (highest == USHRT_MAX)
+		gain = 1;
+	else
+		gain = bh1730_gain_multiplier(bh1730);
+
+	highest = (highest * BH1730_MAX_GAIN_MULTIPLIER) / gain;
+
+	/*
+	 * Find the lowest gain multiplier which puts the measured values
+	 * above 1024. This threshold is chosen to match the gap between 2X
+	 * multiplier and 64X (next available) while keeping some margin.
+	 */
+	for (i = BH1730_GAIN_1X; i < BH1730_GAIN_128X; ++i) {
+		int adj = highest * gain_setting_to_multiplier(i) /
+				BH1730_MAX_GAIN_MULTIPLIER;
+
+		if (adj >= 1024)
+			break;
+	}
+
+	if (i != bh1730->gain) {
+		ret = bh1730_set_gain(bh1730, i);
+		if (ret < 0)
+			return ret;
+
+		bh1730_wait_for_next_measurement(bh1730);
+	}
+
+	return 0;
+}
+
+static int bh1730_get_millilux(struct bh1730_data *bh1730)
+{
+	int visible, ir, visible_coef, ir_coef;
+	u64 millilux;
+
+	visible = bh1730_read_word(bh1730, BH1730_REG_DATA0LOW);
+	if (visible < 0)
+		return visible;
+
+	ir = bh1730_read_word(bh1730, BH1730_REG_DATA1LOW);
+	if (ir < 0)
+		return ir;
+
+	if (ir * 1000 / visible < 500) {
+		visible_coef = 5002;
+		ir_coef = 7502;
+	} else if (ir * 1000 / visible < 754) {
+		visible_coef = 2250;
+		ir_coef = 2000;
+	} else if (ir * 1000 / visible < 1029) {
+		visible_coef = 1999;
+		ir_coef = 1667;
+	} else if (ir * 1000 / visible < 1373) {
+		visible_coef = 885;
+		ir_coef = 583;
+	} else if (ir * 1000 / visible < 1879) {
+		visible_coef = 309;
+		ir_coef = 165;
+	} else {
+		return 0;
+	}
+
+	millilux = 103ULL * (visible_coef * visible - ir_coef * ir);
+	millilux *= USEC_PER_MSEC;
+	do_div(millilux, bh1730_itime_us(bh1730));
+	do_div(millilux, bh1730_gain_multiplier(bh1730));
+
+	/*
+	 * Overflow here can only happen in extreme conditions:
+	 * - Completely saturated visible light sensor and no measured IR.
+	 * - Integration time < 16ms (driver currently defaults to 150ms).
+	 */
+	if (millilux > INT_MAX)
+		return -ERANGE;
+
+	return (int)millilux;
+}
+
+static int bh1730_power_on(struct bh1730_data *bh1730)
+{
+	return bh1730_write(bh1730, BH1730_REG_CONTROL,
+			    BH1730_CONTROL_POWER_ON | BH1730_CONTROL_MEASURE);
+}
+
+static int bh1730_set_defaults(struct bh1730_data *bh1730)
+{
+	int ret;
+
+	ret = bh1730_set_gain(bh1730, BH1730_GAIN_1X);
+	if (ret < 0)
+		return ret;
+
+	ret = bh1730_set_integration_time_ms(bh1730, BH1730_DEFAULT_INTEG_MS);
+	if (ret < 0)
+		return ret;
+
+	bh1730_wait_for_next_measurement(bh1730);
+
+	return 0;
+}
+
+static int bh1730_power_off(struct bh1730_data *bh1730)
+{
+	return bh1730_write(bh1730, BH1730_REG_CONTROL, 0);
+}
+
+static int bh1730_read_raw(struct iio_dev *indio_dev,
+			   struct iio_chan_spec const *chan,
+			   int *val, int *val2, long mask)
+{
+	struct bh1730_data *bh1730 = iio_priv(indio_dev);
+	int data_reg, ret;
+
+	ret = bh1730_adjust_gain(bh1730);
+	if (ret < 0)
+		return ret;
+
+	switch (mask) {
+	case IIO_CHAN_INFO_PROCESSED:
+		ret = bh1730_get_millilux(bh1730);
+		if (ret < 0)
+			return ret;
+		*val = ret / 1000;
+		*val2 = (ret % 1000) * 1000;
+		return IIO_VAL_INT_PLUS_MICRO;
+	case IIO_CHAN_INFO_RAW:
+		switch (chan->channel2) {
+		case IIO_MOD_LIGHT_CLEAR:
+			data_reg = BH1730_REG_DATA0LOW;
+			break;
+		case IIO_MOD_LIGHT_IR:
+			data_reg = BH1730_REG_DATA1LOW;
+			break;
+		default:
+			return -EINVAL;
+		}
+		ret = bh1730_read_word(bh1730, data_reg);
+		if (ret < 0)
+			return ret;
+		ret = ret * 1000 / bh1730_gain_multiplier(bh1730);
+		*val = ret / 1000;
+		*val2 = (ret % 1000) * 1000;
+		return IIO_VAL_INT_PLUS_MICRO;
+	default:
+		return -EINVAL;
+	}
+}
+
+static const struct iio_info bh1730_info = {
+	.read_raw = bh1730_read_raw,
+};
+
+static const struct iio_chan_spec bh1730_channels[] = {
+	{
+		.type = IIO_LIGHT,
+		.info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED),
+	},
+	{
+		.type = IIO_INTENSITY,
+		.modified = 1,
+		.channel2 = IIO_MOD_LIGHT_CLEAR,
+		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
+	},
+	{
+		.type = IIO_INTENSITY,
+		.modified = 1,
+		.channel2 = IIO_MOD_LIGHT_IR,
+		.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
+	},
+};
+
+static int bh1730_probe(struct i2c_client *client)
+{
+	struct bh1730_data *bh1730;
+	struct i2c_adapter *adapter = to_i2c_adapter(client->dev.parent);
+	struct iio_dev *indio_dev;
+	int ret;
+
+	if (!i2c_check_functionality(adapter, I2C_FUNC_SMBUS_BYTE))
+		return -EIO;
+
+	indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*bh1730));
+	if (!indio_dev)
+		return -ENOMEM;
+
+	bh1730 = iio_priv(indio_dev);
+	bh1730->client = client;
+	i2c_set_clientdata(client, indio_dev);
+
+	ret = bh1730_power_on(bh1730);
+	if (ret < 0)
+		return ret;
+
+	ret = bh1730_set_defaults(bh1730);
+	if (ret < 0)
+		return ret;
+
+	indio_dev->dev.parent = &client->dev;
+	indio_dev->info = &bh1730_info;
+	indio_dev->name = "bh1730";
+	indio_dev->channels = bh1730_channels;
+	indio_dev->num_channels = ARRAY_SIZE(bh1730_channels);
+	indio_dev->modes = INDIO_DIRECT_MODE;
+
+	ret = iio_device_register(indio_dev);
+	if (ret)
+		goto out_power_off;
+	return 0;
+
+out_power_off:
+	bh1730_power_off(bh1730);
+	return ret;
+}
+
+static int bh1730_remove(struct i2c_client *client)
+{
+	struct iio_dev *indio_dev = i2c_get_clientdata(client);
+	struct bh1730_data *bh1730 = iio_priv(indio_dev);
+
+	iio_device_unregister(indio_dev);
+	return bh1730_power_off(bh1730);
+}
+
+static const struct of_device_id of_bh1730_match[] = {
+	{ .compatible = "rohm,bh1730fvc" },
+	{},
+};
+MODULE_DEVICE_TABLE(of, of_bh1730_match);
+
+static struct i2c_driver bh1730_driver = {
+	.probe_new = bh1730_probe,
+	.remove = bh1730_remove,
+	.driver = {
+		.name = "bh1730",
+		.of_match_table = of_bh1730_match,
+	},
+};
+module_i2c_driver(bh1730_driver);
+
+MODULE_AUTHOR("Pierre Bourdon <delroth@xxxxxxxxxx>");
+MODULE_DESCRIPTION("ROHM BH1730FVC driver");
+MODULE_LICENSE("GPL v2");
-- 
2.16.2.395.g2e18187dfd-goog

--
To unsubscribe from this list: send the line "unsubscribe devicetree" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html



[Index of Archives]     [Device Tree Compilter]     [Device Tree Spec]     [Linux Driver Backports]     [Video for Linux]     [Linux USB Devel]     [Linux PCI Devel]     [Linux Audio Users]     [Linux Kernel]     [Linux SCSI]     [XFree86]     [Yosemite Backpacking]


  Powered by Linux