drivers: sensors: add a sensor driver for TCS3400

Add a sensor driver for the TCS3400 color light-to-digital converter.

Signed-off-by: Fabio Baltieri <fabiobaltieri@google.com>
diff --git a/drivers/sensor/CMakeLists.txt b/drivers/sensor/CMakeLists.txt
index a9294cc..bfb08e4 100644
--- a/drivers/sensor/CMakeLists.txt
+++ b/drivers/sensor/CMakeLists.txt
@@ -140,6 +140,7 @@
 add_subdirectory_ifdef(CONFIG_TMD2620       tmd2620)
 add_subdirectory_ifdef(CONFIG_NTC_THERMISTOR ntc_thermistor)
 add_subdirectory_ifdef(CONFIG_S11059            s11059)
+add_subdirectory_ifdef(CONFIG_TCS3400 tcs3400)
 
 if(CONFIG_USERSPACE OR CONFIG_SENSOR_SHELL OR CONFIG_SENSOR_SHELL_BATTERY)
 # The above if() is needed or else CMake would complain about
diff --git a/drivers/sensor/Kconfig b/drivers/sensor/Kconfig
index 37571d2..8ae89ca 100644
--- a/drivers/sensor/Kconfig
+++ b/drivers/sensor/Kconfig
@@ -319,4 +319,6 @@
 
 source "drivers/sensor/s11059/Kconfig"
 
+source "drivers/sensor/tcs3400/Kconfig"
+
 endif # SENSOR
diff --git a/drivers/sensor/tcs3400/CMakeLists.txt b/drivers/sensor/tcs3400/CMakeLists.txt
new file mode 100644
index 0000000..f34ab54
--- /dev/null
+++ b/drivers/sensor/tcs3400/CMakeLists.txt
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: Apache-2.0
+
+zephyr_library()
+
+zephyr_library_sources(tcs3400.c)
diff --git a/drivers/sensor/tcs3400/Kconfig b/drivers/sensor/tcs3400/Kconfig
new file mode 100644
index 0000000..25f421d
--- /dev/null
+++ b/drivers/sensor/tcs3400/Kconfig
@@ -0,0 +1,10 @@
+# Copyright 2023 Google LLC
+# SPDX-License-Identifier: Apache-2.0
+
+config TCS3400
+	bool "TCS3400 Sensor"
+	default y
+	depends on DT_HAS_AMS_TCS3400_ENABLED
+	select I2C
+	help
+	  Enable driver for TCS3400 sensors.
diff --git a/drivers/sensor/tcs3400/tcs3400.c b/drivers/sensor/tcs3400/tcs3400.c
new file mode 100644
index 0000000..3673590
--- /dev/null
+++ b/drivers/sensor/tcs3400/tcs3400.c
@@ -0,0 +1,304 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#define DT_DRV_COMPAT ams_tcs3400
+
+#include <zephyr/drivers/gpio.h>
+#include <zephyr/drivers/i2c.h>
+#include <zephyr/drivers/sensor.h>
+#include <zephyr/drivers/sensor/tcs3400.h>
+#include <zephyr/sys/byteorder.h>
+#include <zephyr/sys/util.h>
+
+#include <zephyr/logging/log.h>
+LOG_MODULE_REGISTER(tcs3400, CONFIG_SENSOR_LOG_LEVEL);
+
+#define TCS3400_ENABLE_REG 0x80
+#define TCS3400_ENABLE_AIEN BIT(4)
+#define TCS3400_ENABLE_AEN BIT(1)
+#define TCS3400_ENABLE_PON BIT(0)
+
+#define TCS3400_ATIME_REG 0x81
+
+#define TCS3400_PERS_REG 0x8c
+
+#define TCS3400_CONFIG_REG 0x8d
+
+#define TCS3400_CONTROL_REG 0x8f
+
+#define TCS3400_ID_REG 0x92
+#define TCS3400_ID_1 0x90
+#define TCS3400_ID_2 0x93
+
+#define TCS3400_STATUS_REG 0x93
+#define TCS3400_STATUS_AVALID BIT(0)
+
+#define TCS3400_CDATAL_REG 0x94
+#define TCS3400_CDATAH_REG 0x95
+#define TCS3400_RDATAL_REG 0x96
+#define TCS3400_RDATAH_REG 0x97
+#define TCS3400_GDATAL_REG 0x98
+#define TCS3400_GDATAH_REG 0x99
+#define TCS3400_BDATAL_REG 0x9A
+#define TCS3400_BDATAH_REG 0x9B
+
+#define TCS3400_AICLEAR_REG 0xe7
+
+/* Default values */
+#define TCS3400_DEFAULT_ENABLE 0x00
+#define TCS3400_DEFAULT_ATIME 0xff
+#define TCS3400_DEFAULT_PERS 0x00
+#define TCS3400_DEFAULT_CONFIG 0x00
+#define TCS3400_DEFAULT_CONTROL 0x00
+#define TCS3400_AICLEAR_RESET 0x00
+
+struct tcs3400_config {
+	struct i2c_dt_spec i2c;
+	struct gpio_dt_spec int_gpio;
+};
+
+struct tcs3400_data {
+	struct gpio_callback gpio_cb;
+	const struct device *dev;
+
+	uint16_t sample_crgb[4];
+
+	struct k_sem data_sem;
+};
+
+static void tcs3400_setup_int(const struct tcs3400_config *config, bool enable)
+{
+	unsigned int flags = enable ? GPIO_INT_EDGE_TO_ACTIVE : GPIO_INT_DISABLE;
+
+	gpio_pin_interrupt_configure_dt(&config->int_gpio, flags);
+}
+
+static void tcs3400_gpio_callback(const struct device *dev,
+				  struct gpio_callback *cb, uint32_t pins)
+{
+	struct tcs3400_data *data = CONTAINER_OF(cb, struct tcs3400_data,
+						 gpio_cb);
+
+	tcs3400_setup_int(data->dev->config, false);
+
+	k_sem_give(&data->data_sem);
+}
+
+static int tcs3400_sample_fetch(const struct device *dev,
+				enum sensor_channel chan)
+{
+	const struct tcs3400_config *cfg = dev->config;
+	struct tcs3400_data *data = dev->data;
+	int ret;
+	uint8_t status;
+
+	if (chan != SENSOR_CHAN_ALL) {
+		LOG_ERR("Unsupported sensor channel");
+		return -ENOTSUP;
+	}
+
+	tcs3400_setup_int(cfg, true);
+
+	ret = i2c_reg_write_byte_dt(&cfg->i2c, TCS3400_ENABLE_REG,
+				    TCS3400_ENABLE_AIEN | TCS3400_ENABLE_AEN |
+				    TCS3400_ENABLE_PON);
+	if (ret) {
+		return ret;
+	}
+
+	k_sem_take(&data->data_sem, K_FOREVER);
+
+	ret = i2c_reg_read_byte_dt(&cfg->i2c, TCS3400_STATUS_REG, &status);
+	if (ret) {
+		return ret;
+	}
+
+	if (status & TCS3400_STATUS_AVALID) {
+		ret = i2c_burst_read_dt(&cfg->i2c, TCS3400_CDATAL_REG,
+					(uint8_t *)&data->sample_crgb,
+					sizeof(data->sample_crgb));
+		if (ret) {
+			return ret;
+		}
+	} else {
+		LOG_ERR("Unexpected status: %02x", status);
+	}
+
+	ret = i2c_reg_write_byte_dt(&cfg->i2c, TCS3400_ENABLE_REG, 0);
+	if (ret) {
+		return ret;
+	}
+
+	ret = i2c_reg_write_byte_dt(&cfg->i2c, TCS3400_AICLEAR_REG, 0);
+	if (ret) {
+		return ret;
+	}
+
+	return 0;
+}
+
+static int tcs3400_channel_get(const struct device *dev,
+			       enum sensor_channel chan,
+			       struct sensor_value *val)
+{
+	struct tcs3400_data *data = dev->data;
+
+	switch (chan) {
+	case SENSOR_CHAN_LIGHT:
+		val->val1 = sys_le16_to_cpu(data->sample_crgb[0]);
+		val->val2 = 0;
+		break;
+	case SENSOR_CHAN_RED:
+		val->val1 = sys_le16_to_cpu(data->sample_crgb[1]);
+		val->val2 = 0;
+		break;
+	case SENSOR_CHAN_GREEN:
+		val->val1 = sys_le16_to_cpu(data->sample_crgb[2]);
+		val->val2 = 0;
+		break;
+	case SENSOR_CHAN_BLUE:
+		val->val1 = sys_le16_to_cpu(data->sample_crgb[3]);
+		val->val2 = 0;
+		break;
+	default:
+		return -ENOTSUP;
+	}
+
+	return 0;
+}
+
+static int tcs3400_attr_set(const struct device *dev,
+			    enum sensor_channel chan,
+			    enum sensor_attribute attr,
+			    const struct sensor_value *val)
+{
+	const struct tcs3400_config *cfg = dev->config;
+	int ret;
+	uint8_t reg_val;
+
+	switch (attr) {
+	case SENSOR_ATTR_TCS3400_INTEGRATION_CYCLES:
+		if (!IN_RANGE(val->val1, 1, 256)) {
+			return -EINVAL;
+		}
+		reg_val = UINT8_MAX - val->val1 + 1;
+		ret = i2c_reg_write_byte_dt(&cfg->i2c,
+					    TCS3400_ATIME_REG, reg_val);
+		if (ret) {
+			return ret;
+		}
+		break;
+	default:
+		return -ENOTSUP;
+	}
+
+	return 0;
+}
+
+static int tcs3400_sensor_setup(const struct device *dev)
+{
+	const struct tcs3400_config *cfg = dev->config;
+	uint8_t chip_id;
+	int ret;
+	struct {
+		uint8_t reg_addr;
+		uint8_t value;
+	} reset_regs[] = {
+		{TCS3400_ENABLE_REG, TCS3400_DEFAULT_ENABLE},
+		{TCS3400_AICLEAR_REG, TCS3400_AICLEAR_RESET},
+		{TCS3400_ATIME_REG, TCS3400_DEFAULT_ATIME},
+		{TCS3400_PERS_REG, TCS3400_DEFAULT_PERS},
+		{TCS3400_CONFIG_REG, TCS3400_DEFAULT_CONFIG},
+		{TCS3400_CONTROL_REG, TCS3400_DEFAULT_CONTROL},
+	};
+
+	ret = i2c_reg_read_byte_dt(&cfg->i2c, TCS3400_ID_REG, &chip_id);
+	if (ret) {
+		LOG_DBG("Failed to read chip id: %d", ret);
+		return ret;
+	}
+
+	if (!((chip_id == TCS3400_ID_1) || (chip_id == TCS3400_ID_2))) {
+		LOG_DBG("Invalid chip id: %02x", chip_id);
+		return -EIO;
+	}
+
+	LOG_INF("chip id: 0x%x", chip_id);
+
+	for (size_t i = 0; i < ARRAY_SIZE(reset_regs); i++) {
+		ret = i2c_reg_write_byte_dt(&cfg->i2c, reset_regs[i].reg_addr,
+					    reset_regs[i].value);
+		if (ret) {
+			LOG_ERR("Failed to set default register: %02x",
+				reset_regs[i].reg_addr);
+			return ret;
+		}
+	}
+
+	return 0;
+}
+
+static const struct sensor_driver_api tcs3400_api = {
+	.sample_fetch = tcs3400_sample_fetch,
+	.channel_get = tcs3400_channel_get,
+	.attr_set = tcs3400_attr_set,
+};
+
+static int tcs3400_init(const struct device *dev)
+{
+	const struct tcs3400_config *cfg = dev->config;
+	struct tcs3400_data *data = dev->data;
+	int ret;
+
+	k_sem_init(&data->data_sem, 0, K_SEM_MAX_LIMIT);
+	data->dev = dev;
+
+	if (!i2c_is_ready_dt(&cfg->i2c)) {
+		LOG_ERR("I2C bus is not ready");
+		return -ENODEV;
+	}
+
+	ret = tcs3400_sensor_setup(dev);
+	if (ret < 0) {
+		LOG_ERR("Failed to setup device");
+		return ret;
+	}
+
+	if (!gpio_is_ready_dt(&cfg->int_gpio)) {
+		LOG_ERR("Interrupt GPIO device not ready");
+		return -ENODEV;
+	}
+
+	ret = gpio_pin_configure_dt(&cfg->int_gpio, GPIO_INPUT);
+	if (ret < 0) {
+		LOG_ERR("Failed to configure interrupt pin");
+		return ret;
+	}
+
+	gpio_init_callback(&data->gpio_cb, tcs3400_gpio_callback,
+			   BIT(cfg->int_gpio.pin));
+
+	ret = gpio_add_callback(cfg->int_gpio.port, &data->gpio_cb);
+	if (ret < 0) {
+		LOG_ERR("Failed to set GPIO callback");
+		return ret;
+	}
+
+	return 0;
+}
+
+#define TCS3400_INIT(n) \
+	static struct tcs3400_data tcs3400_data_##n; \
+	static const struct tcs3400_config tcs3400_config_##n = { \
+		.i2c = I2C_DT_SPEC_INST_GET(n), \
+		.int_gpio = GPIO_DT_SPEC_INST_GET(n, int_gpios), \
+	}; \
+	SENSOR_DEVICE_DT_INST_DEFINE(n, &tcs3400_init, NULL, \
+				     &tcs3400_data_##n, &tcs3400_config_##n, \
+				     POST_KERNEL, CONFIG_SENSOR_INIT_PRIORITY, \
+				     &tcs3400_api);
+
+DT_INST_FOREACH_STATUS_OKAY(TCS3400_INIT)
diff --git a/dts/bindings/sensor/ams,tcs3400.yaml b/dts/bindings/sensor/ams,tcs3400.yaml
new file mode 100644
index 0000000..609efe8
--- /dev/null
+++ b/dts/bindings/sensor/ams,tcs3400.yaml
@@ -0,0 +1,15 @@
+# Copyright 2023 Google LLC
+# SPDX-License-Identifier: Apache-2.0
+
+description: AMS TCS3400 Color Light-to-Digital Converter
+
+compatible: "ams,tcs3400"
+
+include: [sensor-device.yaml, i2c-device.yaml]
+
+properties:
+  int-gpios:
+    type: phandle-array
+    required: true
+    description: |
+      INT pin GPIO identifier, open-drain, active low.
diff --git a/include/zephyr/drivers/sensor/tcs3400.h b/include/zephyr/drivers/sensor/tcs3400.h
new file mode 100644
index 0000000..1c92cf1
--- /dev/null
+++ b/include/zephyr/drivers/sensor/tcs3400.h
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#ifndef ZEPHYR_INCLUDE_DRIVERS_SENSOR_TCS3400_H_
+#define ZEPHYR_INCLUDE_DRIVERS_SENSOR_TCS3400_H_
+
+#include <zephyr/drivers/sensor.h>
+
+enum sensor_attribute_tcs3400 {
+	/** RGBC Integration Cycles */
+	SENSOR_ATTR_TCS3400_INTEGRATION_CYCLES = SENSOR_ATTR_PRIV_START,
+};
+
+#endif /* ZEPHYR_INCLUDE_DRIVERS_SENSOR_TCS3400_H_ */
diff --git a/tests/drivers/build_all/sensor/i2c.dtsi b/tests/drivers/build_all/sensor/i2c.dtsi
index 8ac0e38..4023fc8 100644
--- a/tests/drivers/build_all/sensor/i2c.dtsi
+++ b/tests/drivers/build_all/sensor/i2c.dtsi
@@ -739,3 +739,9 @@
 	compatible = "microchip,mcp9600";
 	reg = <0x71>;
 };
+
+test_i2c_tcs3400: tcs3400@72 {
+	compatible = "ams,tcs3400";
+	reg = <0x72>;
+	int-gpios = <&test_gpio 0 0>;
+};