@@ -0,0 +1,383 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Capella CM3217 ambient light sensor driver
+ *
+ * The CM3217 has no registers. It answers on two fixed I2C addresses:
+ * writing a byte to 0x10 or 0x11 sets command byte 1 or 2, reading a byte
+ * from 0x10 or 0x11 returns the MSB or LSB of the 16-bit light count.
+ *
+ * Copyright (C) 2026 Patrick Fischer <admin@tsv-stahl-riesa.de>
+ */
+
+#include <linux/array_size.h>
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/cleanup.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/i2c.h>
+#include <linux/ktime.h>
+#include <linux/math64.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/pm.h>
+#include <linux/regulator/consumer.h>
+#include <linux/types.h>
+#include <linux/units.h>
+
+#include <linux/iio/iio.h>
+
+#define CM3217_ADDR_CMD2_LSB 0x11
+
+#define CM3217_CMD1_RSVD BIT(5) /* reserved, set to 1 */
+#define CM3217_CMD1_IT_T GENMASK(3, 2) /* 1/2 T, 1 T, 2 T, 4 T */
+#define CM3217_CMD1_WDM BIT(1) /* set to 1 like the vendor driver */
+#define CM3217_CMD1_SD BIT(0) /* shutdown when set */
+
+#define CM3217_CMD2_FD_IT GENMASK(7, 5)
+
+#define CM3217_IT_T_2T 2
+#define CM3217_FD_IT_100MS 5
+
+/* 0.01875 lux per count at 4 T and 800 ms, the finest resolution */
+#define CM3217_NLUX_MS 15000000000ULL
+
+#define CM3217_MARGIN_MS 10
+
+/* FD_IT settings in milliseconds, indexed by the field value */
+static const unsigned int cm3217_fd_it_ms[] = {
+ 800, 400, 266, 200, 130, 100, 80, 66,
+};
+
+static const int cm3217_int_time_avail[][2] = {
+ { 0, 800000 }, { 0, 400000 }, { 0, 266000 }, { 0, 200000 },
+ { 0, 130000 }, { 0, 100000 }, { 0, 80000 }, { 0, 66000 },
+};
+
+struct cm3217_data {
+ struct i2c_client *client;
+ struct i2c_client *client_lsb;
+ /* protects the command bytes, the scale table and ready */
+ struct mutex lock;
+ u8 cmd1;
+ u8 cmd2;
+ ktime_t ready;
+ int scale_avail[4][2];
+};
+
+static unsigned int cm3217_conv_ms(struct cm3217_data *data)
+ __must_hold(&data->lock)
+{
+ unsigned int fd_it = FIELD_GET(CM3217_CMD2_FD_IT, data->cmd2);
+ unsigned int it_t = FIELD_GET(CM3217_CMD1_IT_T, data->cmd1);
+
+ return (cm3217_fd_it_ms[fd_it] << it_t) / 2;
+}
+
+static void cm3217_update_scale_avail(struct cm3217_data *data)
+ __must_hold(&data->lock)
+{
+ unsigned int fd_it = FIELD_GET(CM3217_CMD2_FD_IT, data->cmd2);
+ unsigned int i;
+ u32 rem;
+ u64 nlux;
+
+ for (i = 0; i < ARRAY_SIZE(data->scale_avail); i++) {
+ nlux = div_u64(CM3217_NLUX_MS << (3 - i), cm3217_fd_it_ms[fd_it]);
+ data->scale_avail[i][0] = div_u64_rem(nlux, NANO, &rem);
+ data->scale_avail[i][1] = rem;
+ }
+}
+
+/*
+ * A new setting only takes effect when the conversion in progress ends,
+ * which can take up to 3.2 s. Leaving shutdown starts a new conversion
+ * right away, so pass through shutdown on every change.
+ */
+static int cm3217_write_cmd(struct cm3217_data *data, u8 cmd1, u8 cmd2)
+ __must_hold(&data->lock)
+{
+ unsigned int conv_ms;
+ int ret;
+
+ ret = i2c_smbus_write_byte(data->client, cmd1 | CM3217_CMD1_SD);
+ if (ret)
+ return ret;
+
+ ret = i2c_smbus_write_byte(data->client_lsb, cmd2);
+ if (ret)
+ return ret;
+
+ ret = i2c_smbus_write_byte(data->client, cmd1);
+ if (ret)
+ return ret;
+
+ data->cmd1 = cmd1;
+ data->cmd2 = cmd2;
+ cm3217_update_scale_avail(data);
+
+ /* the first count is ready after one conversion */
+ conv_ms = cm3217_conv_ms(data);
+ data->ready = ktime_add_ms(ktime_get(),
+ conv_ms + conv_ms / 10 + CM3217_MARGIN_MS);
+
+ return 0;
+}
+
+static int cm3217_read_count(struct cm3217_data *data, int *val)
+ __must_hold(&data->lock)
+{
+ s64 wait_ms;
+ int lsb, msb;
+
+ wait_ms = ktime_ms_delta(data->ready, ktime_get());
+ if (wait_ms > 0)
+ msleep(wait_ms);
+
+ lsb = i2c_smbus_read_byte(data->client_lsb);
+ if (lsb < 0)
+ return lsb;
+
+ msb = i2c_smbus_read_byte(data->client);
+ if (msb < 0)
+ return msb;
+
+ *val = msb << 8 | lsb;
+
+ return 0;
+}
+
+static const struct iio_chan_spec cm3217_channels[] = {
+ {
+ .type = IIO_LIGHT,
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
+ BIT(IIO_CHAN_INFO_SCALE),
+ .info_mask_separate_available = BIT(IIO_CHAN_INFO_SCALE),
+ .info_mask_shared_by_all = BIT(IIO_CHAN_INFO_INT_TIME),
+ .info_mask_shared_by_all_available = BIT(IIO_CHAN_INFO_INT_TIME),
+ },
+};
+
+static int cm3217_read_raw(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan,
+ int *val, int *val2, long mask)
+{
+ struct cm3217_data *data = iio_priv(indio_dev);
+ unsigned int idx;
+ int ret;
+
+ guard(mutex)(&data->lock);
+
+ switch (mask) {
+ case IIO_CHAN_INFO_RAW:
+ ret = cm3217_read_count(data, val);
+ if (ret)
+ return ret;
+ return IIO_VAL_INT;
+ case IIO_CHAN_INFO_SCALE:
+ idx = FIELD_GET(CM3217_CMD1_IT_T, data->cmd1);
+ *val = data->scale_avail[idx][0];
+ *val2 = data->scale_avail[idx][1];
+ return IIO_VAL_INT_PLUS_NANO;
+ case IIO_CHAN_INFO_INT_TIME:
+ idx = FIELD_GET(CM3217_CMD2_FD_IT, data->cmd2);
+ *val = cm3217_int_time_avail[idx][0];
+ *val2 = cm3217_int_time_avail[idx][1];
+ return IIO_VAL_INT_PLUS_MICRO;
+ default:
+ return -EINVAL;
+ }
+}
+
+static int cm3217_read_avail(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan,
+ const int **vals, int *type, int *length,
+ long mask)
+{
+ struct cm3217_data *data = iio_priv(indio_dev);
+
+ switch (mask) {
+ case IIO_CHAN_INFO_SCALE:
+ *vals = (const int *)data->scale_avail;
+ *length = 2 * ARRAY_SIZE(data->scale_avail);
+ *type = IIO_VAL_INT_PLUS_NANO;
+ return IIO_AVAIL_LIST;
+ case IIO_CHAN_INFO_INT_TIME:
+ *vals = (const int *)cm3217_int_time_avail;
+ *length = 2 * ARRAY_SIZE(cm3217_int_time_avail);
+ *type = IIO_VAL_INT_PLUS_MICRO;
+ return IIO_AVAIL_LIST;
+ default:
+ return -EINVAL;
+ }
+}
+
+static int cm3217_write_raw(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan,
+ int val, int val2, long mask)
+{
+ struct cm3217_data *data = iio_priv(indio_dev);
+ unsigned int i;
+ u8 cmd1, cmd2;
+
+ guard(mutex)(&data->lock);
+
+ switch (mask) {
+ case IIO_CHAN_INFO_SCALE:
+ for (i = 0; i < ARRAY_SIZE(data->scale_avail); i++) {
+ if (data->scale_avail[i][0] == val &&
+ data->scale_avail[i][1] == val2)
+ break;
+ }
+ if (i == ARRAY_SIZE(data->scale_avail))
+ return -EINVAL;
+
+ cmd1 = data->cmd1 & ~CM3217_CMD1_IT_T;
+ cmd1 |= FIELD_PREP(CM3217_CMD1_IT_T, i);
+
+ return cm3217_write_cmd(data, cmd1, data->cmd2);
+ case IIO_CHAN_INFO_INT_TIME:
+ for (i = 0; i < ARRAY_SIZE(cm3217_int_time_avail); i++) {
+ if (cm3217_int_time_avail[i][0] == val &&
+ cm3217_int_time_avail[i][1] == val2)
+ break;
+ }
+ if (i == ARRAY_SIZE(cm3217_int_time_avail))
+ return -EINVAL;
+
+ cmd2 = FIELD_PREP(CM3217_CMD2_FD_IT, i);
+
+ return cm3217_write_cmd(data, data->cmd1, cmd2);
+ default:
+ return -EINVAL;
+ }
+}
+
+static int cm3217_write_raw_get_fmt(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan,
+ long mask)
+{
+ switch (mask) {
+ case IIO_CHAN_INFO_SCALE:
+ return IIO_VAL_INT_PLUS_NANO;
+ case IIO_CHAN_INFO_INT_TIME:
+ return IIO_VAL_INT_PLUS_MICRO;
+ default:
+ return -EINVAL;
+ }
+}
+
+static const struct iio_info cm3217_info = {
+ .read_raw = cm3217_read_raw,
+ .read_avail = cm3217_read_avail,
+ .write_raw = cm3217_write_raw,
+ .write_raw_get_fmt = cm3217_write_raw_get_fmt,
+};
+
+static void cm3217_power_off(void *_data)
+{
+ struct cm3217_data *data = _data;
+
+ i2c_smbus_write_byte(data->client, data->cmd1 | CM3217_CMD1_SD);
+}
+
+static int cm3217_probe(struct i2c_client *client)
+{
+ struct device *dev = &client->dev;
+ struct cm3217_data *data;
+ struct iio_dev *indio_dev;
+ u8 cmd1, cmd2;
+ int ret;
+
+ indio_dev = devm_iio_device_alloc(dev, sizeof(*data));
+ if (!indio_dev)
+ return -ENOMEM;
+
+ data = iio_priv(indio_dev);
+ data->client = client;
+ i2c_set_clientdata(client, indio_dev);
+
+ ret = devm_mutex_init(dev, &data->lock);
+ if (ret)
+ return ret;
+
+ ret = devm_regulator_get_enable(dev, "vdd");
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to enable vdd supply\n");
+
+ data->client_lsb = devm_i2c_new_dummy_device(dev, client->adapter,
+ CM3217_ADDR_CMD2_LSB);
+ if (IS_ERR(data->client_lsb))
+ return dev_err_probe(dev, PTR_ERR(data->client_lsb),
+ "failed to register second address\n");
+
+ /* Start with the setting of the vendor driver: 2 T, 100 ms */
+ cmd1 = CM3217_CMD1_RSVD | CM3217_CMD1_WDM |
+ FIELD_PREP(CM3217_CMD1_IT_T, CM3217_IT_T_2T);
+ cmd2 = FIELD_PREP(CM3217_CMD2_FD_IT, CM3217_FD_IT_100MS);
+
+ scoped_guard(mutex, &data->lock)
+ ret = cm3217_write_cmd(data, cmd1, cmd2);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to power on\n");
+
+ ret = devm_add_action_or_reset(dev, cm3217_power_off, data);
+ if (ret)
+ return ret;
+
+ indio_dev->name = "cm3217";
+ indio_dev->info = &cm3217_info;
+ indio_dev->channels = cm3217_channels;
+ indio_dev->num_channels = ARRAY_SIZE(cm3217_channels);
+ indio_dev->modes = INDIO_DIRECT_MODE;
+
+ return devm_iio_device_register(dev, indio_dev);
+}
+
+static int cm3217_suspend(struct device *dev)
+{
+ struct cm3217_data *data = iio_priv(dev_get_drvdata(dev));
+
+ guard(mutex)(&data->lock);
+
+ return i2c_smbus_write_byte(data->client, data->cmd1 | CM3217_CMD1_SD);
+}
+
+static int cm3217_resume(struct device *dev)
+{
+ struct cm3217_data *data = iio_priv(dev_get_drvdata(dev));
+
+ guard(mutex)(&data->lock);
+
+ return cm3217_write_cmd(data, data->cmd1, data->cmd2);
+}
+
+static DEFINE_SIMPLE_DEV_PM_OPS(cm3217_pm_ops, cm3217_suspend, cm3217_resume);
+
+static const struct i2c_device_id cm3217_id[] = {
+ { .name = "cm3217" },
+ { }
+};
+MODULE_DEVICE_TABLE(i2c, cm3217_id);
+
+static const struct of_device_id cm3217_of_match[] = {
+ { .compatible = "capella,cm3217" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, cm3217_of_match);
+
+static struct i2c_driver cm3217_driver = {
+ .driver = {
+ .name = "cm3217",
+ .of_match_table = cm3217_of_match,
+ .pm = pm_sleep_ptr(&cm3217_pm_ops),
+ },
+ .probe = cm3217_probe,
+ .id_table = cm3217_id,
+};
+module_i2c_driver(cm3217_driver);
+
+MODULE_AUTHOR("Patrick Fischer <admin@tsv-stahl-riesa.de>");
+MODULE_DESCRIPTION("Capella CM3217 ambient light sensor driver");
+MODULE_LICENSE("GPL");