Re: [PATCH] iio: light: ltr390: Add configurable gain, resolution and ALS reading

From: Abhash jha
Date: Fri Jul 26 2024 - 08:55:45 EST


On Sat, Jul 20, 2024 at 9:26 PM Jonathan Cameron <jic23@xxxxxxxxxx> wrote:
>
> On Thu, 18 Jul 2024 16:19:45 +0530
> Abhash Jha <abhashkumarjha123@xxxxxxxxx> wrote:
>
> > 1) Add support for configuring the gain and resolution(integration time)
> > for the sensor.
> > 2) Add a channel for ALS and provide support for reading the raw and
> > scale values.
> > 3) Add automatic mode switching between UVS and ALS based on the
> > channel type.
> > 4) Calculate 'counts_per_uvi' based on the current gain and integration
> > time.
>
> Hi Abhash,
>
> When a patch lists more than one thing, key thing to think is
> "maybe this should be multiple patches?"
>
> Here at very least separate resolution / gain into one or two patches
> and the new channel support into another.
> Probably yet another patch for point 4,
>
> Various other comments inline.
>
> Jonathan
>
> >
> > Signed-off-by: Abhash Jha <abhashkumarjha123@xxxxxxxxx>
> > ---
> > drivers/iio/light/ltr390.c | 256 ++++++++++++++++++++++++++++++++++---
> > 1 file changed, 238 insertions(+), 18 deletions(-)
> >
> > diff --git a/drivers/iio/light/ltr390.c b/drivers/iio/light/ltr390.c
> > index fff1e8990..56f3c74ae 100644
> > --- a/drivers/iio/light/ltr390.c
> > +++ b/drivers/iio/light/ltr390.c
> > @@ -25,19 +25,33 @@
> > #include <linux/regmap.h>
> >
> > #include <linux/iio/iio.h>
> > -
> > +#include <linux/iio/sysfs.h>
>
> > #include <asm/unaligned.h>
> >
> > #define LTR390_MAIN_CTRL 0x00
> > #define LTR390_PART_ID 0x06
> > #define LTR390_UVS_DATA 0x10
> >
> > +#define LTR390_ALS_DATA 0x0D
> > +#define LTR390_ALS_UVS_GAIN 0x05
> > +#define LTR390_ALS_UVS_MEAS_RATE 0x04
> > +#define LTR390_INT_CFG 0x19
> If these are register addresses put them in numeric order so
> it is easy to compare with a datasheet table
>
> > +
> > #define LTR390_SW_RESET BIT(4)
> > #define LTR390_UVS_MODE BIT(3)
> > #define LTR390_SENSOR_ENABLE BIT(1)
> >
> > #define LTR390_PART_NUMBER_ID 0xb
> >
> > +#define LTR390_ALS_UVS_GAIN_MASK 0x07
> > +#define LTR390_ALS_UVS_INT_TIME_MASK 0x70
> > +#define LTR390_ALS_UVS_INT_TIME_MASK_SHIFT 4
>
> Used FIELD_GET() and FIELD_PREP() then you never
> need a separate SHIFT defintion.
>
> > +
> > +#define LTR390_SET_ALS_MODE 1
> > +#define LTR390_SET_UVS_MODE 2
>
> If these are being use to pick options and not writen to hw
> use an enum. I don't think we care what value they take.
>
>
> > +
> > +#define LTR390_FRACTIONAL_PRECISION 100
> > +
> > /*
> > * At 20-bit resolution (integration time: 400ms) and 18x gain, 2300 counts of
> > * the sensor are equal to 1 UV Index [Datasheet Page#8].
> > @@ -60,6 +74,9 @@ struct ltr390_data {
> > struct i2c_client *client;
> > /* Protects device from simulataneous reads */
> > struct mutex lock;
> > + int mode;
> > + int gain;
> > + int int_time_us;
> > };
> >
> > static const struct regmap_config ltr390_regmap_config = {
> > @@ -87,36 +104,232 @@ static int ltr390_register_read(struct ltr390_data *data, u8 register_address)
> > return get_unaligned_le24(recieve_buffer);
> > }
> >
> > +
> one blank line is neough.
>
> > +static int ltr390_set_mode(struct ltr390_data *data, int mode)
> As suggested above, use an enum for mode. Give than a type name and you
> can use that here.
>
> > +{
> > + if (data->mode == mode)
> > + return 0;
> > +
> > + if (mode == LTR390_SET_ALS_MODE) {
> > + regmap_clear_bits(data->regmap, LTR390_MAIN_CTRL, LTR390_UVS_MODE);
> > + data->mode = LTR390_SET_ALS_MODE;
> > + } else if (mode == LTR390_SET_UVS_MODE) {
> > + regmap_set_bits(data->regmap, LTR390_MAIN_CTRL, LTR390_UVS_MODE);
> > + data->mode = LTR390_SET_UVS_MODE;
> Drop this out of the if / else stack and use
> data->mode = mode;
> A switch statement may be more appropriate here even if it's a few more lines of
> code.
>
> > + } else {
> > + return -EINVAL;
> > + }
> > +
> > + return 0;
> > +}
> > +
> > +static int ltr390_counts_per_uvi(struct ltr390_data *data)
> > +{
> > + int orig_gain = 18;
> > + int orig_int_time = 400;
> > + int divisor = orig_gain * orig_int_time;
> > + int gain = data->gain;
> > +
> > + int int_time_ms = DIV_ROUND_CLOSEST(data->int_time_us, 1000);
> > + int uvi = DIV_ROUND_CLOSEST(2300*gain*int_time_ms, divisor);
>
> Spaces around *
>
> > +
> > + return uvi;
> > +}
> > +
> > static int ltr390_read_raw(struct iio_dev *iio_device,
> > struct iio_chan_spec const *chan, int *val,
> > int *val2, long mask)
> > {
> > - int ret;
> > struct ltr390_data *data = iio_priv(iio_device);
> > + int ret;
> Don't move code unless there is a strong reason. Fine to
> tidy this sort of thing up, but not in a patch doing anything else
> as it becomes noise.
>
> >
> Almost certainly need locking here as concurrent accesses to sysfs
> files will result in mode changing whilst the read has not yet happened.
>
> > switch (mask) {
> > case IIO_CHAN_INFO_RAW:
> > - ret = ltr390_register_read(data, LTR390_UVS_DATA);
> > - if (ret < 0)
> > - return ret;
> > + switch (chan->type) {
> > + case IIO_UVINDEX:
> > + ret = ltr390_set_mode(data, LTR390_SET_UVS_MODE);
> > + if (ret < 0)
> > + return ret;
> > +
> > + ret = ltr390_register_read(data, LTR390_UVS_DATA);
> Fix the alignment - looks like mix of spaces and tabs.
> scripts/checkpatch.pl would have pointed that out.
>
> > + if (ret < 0)
> > + return ret;
> > +
> > + break;
> > +
> > + case IIO_INTENSITY:
> > + ret = ltr390_set_mode(data, LTR390_SET_ALS_MODE);
> > + if (ret < 0)
> > + return ret;
> > +
> > + ret = ltr390_register_read(data, LTR390_ALS_DATA);
> > + if (ret < 0)
> > + return ret;
> > + break;
> > +
> > + default:
> > + ret = -EINVAL;
> return here. Otherwise you overwrite the value below.
>
> > + }
> > +
> > *val = ret;
> > - return IIO_VAL_INT;
> > + ret = IIO_VAL_INT;
> return here and drop the break.
> It is much simpler to follow code if it doesn't unnecessarily not
> return in cases like this as we have to scroll down to see if anything else
> happens.
>
> > + break;
> > +
> > case IIO_CHAN_INFO_SCALE:
> > - *val = LTR390_WINDOW_FACTOR;
> > - *val2 = LTR390_COUNTS_PER_UVI;
> > - return IIO_VAL_FRACTIONAL;
> > + mutex_lock(&data->lock);
> Add appropriate scope using {} and use
> guard(mutex)(&data->lock) as then in error paths you can
> return without unlocking...
> > +
> > + switch (chan->type) {
> > + case IIO_UVINDEX:
> > + ret = ltr390_set_mode(data, LTR390_SET_UVS_MODE);
> > + if (ret < 0)
> mutex held. Result is deadlock. Above scoped unlocking avoids that without
> needing to make sure you unlock in all paths.
>
>
> > + return ret;
> > +
> > + *val = LTR390_WINDOW_FACTOR * LTR390_FRACTIONAL_PRECISION;
> > + *val2 = ltr390_counts_per_uvi(data);
> > + ret = IIO_VAL_FRACTIONAL;
> return here.
>
> > + break;
> > +
> > + case IIO_INTENSITY:
> > + ret = ltr390_set_mode(data, LTR390_SET_ALS_MODE);
> > + if (ret < 0)
> > + return ret;
> > +
> > + *val = LTR390_WINDOW_FACTOR;
> > + *val2 = data->gain;
> > +
> > + ret = IIO_VAL_FRACTIONAL;
> > + break;
> return here.
> > +
> > + default:
> > + ret = -EINVAL;
> return here.
> > + }
> > +
> > + mutex_unlock(&data->lock);
> With guard() change above, not needed.
> But close scope here with }
> > + break;
> > +
> > + case IIO_CHAN_INFO_INT_TIME:
> > + mutex_lock(&data->lock);
> Given all paths other than invalid ones need the lock, maybe just take
> it outside of the switch statement - still use guard() though to avoid
> need to manually unlock.
>
> > + *val = data->int_time_us;
> > + mutex_unlock(&data->lock);
> > + ret = IIO_VAL_INT;
> > + break;
> > +
> > default:
> > - return -EINVAL;
> > + ret = -EINVAL;
> > }
> > +
> > + return ret;
> This is a bad change as now I need to read to end of function in all
> code paths. Some code styles insist on single exit points, but
> the kernel style does not. (not worth a long discussion of why the
> two common styles came about). Keep those early returns.
>
>
> > }
> >
> > -static const struct iio_info ltr390_info = {
> > - .read_raw = ltr390_read_raw,
> > +/* integration time in us */
> > +static const int ltr390_int_time_map_us[] = {400000, 200000, 100000, 50000, 25000, 12500};
> > +static const int ltr390_gain_map[] = {1, 3, 6, 9, 18};
> > +
> > +static IIO_CONST_ATTR_INT_TIME_AVAIL("400000 200000 100000 50000 25000 12500");
> Please use read_avail() callback and the appropriate mask to provide this.
> That enables it to be used from in kernel consumers and enforces the
> ABI without a reviewer having to check what you have aligns.
>
> > +static IIO_CONST_ATTR(gain_available, "1 3 6 9 18");
> Given we don't have a 'gain' control, what is the available applying to?
>
The gain gets controlled by writing to the iio_info_scale attribute,
we write one of the above available values.
So that we can scale the raw ALS and UVI values. I could use
read_avail() for this too for the IIO_INFO_SCALE channel. Should I do
that?
Can you elaborate more on your comment?

> > +
> > +static struct attribute *ltr390_attributes[] = {
> > + &iio_const_attr_integration_time_available.dev_attr.attr,
> > + &iio_const_attr_gain_available.dev_attr.attr,
> > + NULL,
> > +};
> > +
> > +static const struct attribute_group ltr390_attribute_group = {
> > + .attrs = ltr390_attributes,
> > };
> >
> > -static const struct iio_chan_spec ltr390_channel = {
> > +static const struct iio_chan_spec ltr390_channels[] = {
> > + /* UV sensor */
> > + {
> > .type = IIO_UVINDEX,
> > - .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | BIT(IIO_CHAN_INFO_SCALE)
> > + .scan_index = 0,
> > + .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | BIT(IIO_CHAN_INFO_SCALE),
> > + .info_mask_shared_by_all = BIT(IIO_CHAN_INFO_INT_TIME)
> Fix style.
> {
> .type = ...
>
> > + },
> > + /* ALS sensor */
> > + {
> > + .type = IIO_INTENSITY,
> > + .scan_index = 1,
> > + .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | BIT(IIO_CHAN_INFO_SCALE),
> > + .info_mask_shared_by_all = BIT(IIO_CHAN_INFO_INT_TIME)
> > + },
> > +};
> > +
> > +static int ltr390_set_gain(struct ltr390_data *data, int val)
> > +{
> > + int ret, idx;
> > +
> > + for (idx = 0; idx < ARRAY_SIZE(ltr390_gain_map); idx++) {
> > + if (ltr390_gain_map[idx] == val) {
> > + mutex_lock(&data->lock);
> guard here.
> > + ret = regmap_update_bits(data->regmap,
> > + LTR390_ALS_UVS_GAIN,
> > + LTR390_ALS_UVS_GAIN_MASK, idx);
> > + if (!ret)
> if (ret)
> return ret;
> prefer to keep error paths as the out of line ones as if you review
> a lot of code, predictability helps review quickly.
>
> > + data->gain = ltr390_gain_map[idx];
> > +
> > + mutex_unlock(&data->lock);
> > + break;
> > + }
> > + }
> > +
> > + return 0;
> > +}
> > +
> > +static int ltr390_set_int_time(struct ltr390_data *data, int val)
> > +{
> > + int ret, idx;
> > +
> > + for (idx = 0; idx < ARRAY_SIZE(ltr390_int_time_map_us); idx++) {
> > + if (ltr390_int_time_map_us[idx] == val) {
> flip logic to reduce indent.
> if (ltr390_int_time_map_us[idx] != val)
> continue;
>
> guard(mutex)...
>
> > + mutex_lock(&data->lock);
> > + ret = regmap_update_bits(data->regmap,
> > + LTR390_ALS_UVS_MEAS_RATE,
> > + LTR390_ALS_UVS_INT_TIME_MASK,
> > + idx<<LTR390_ALS_UVS_INT_TIME_MASK_SHIFT);
> spaces around <<
> Though FIELD_PREP() probably better solution.
>
> > + if (!ret)
> As in previous funciton.
> > + data->int_time_us = ltr390_int_time_map_us[idx];
> > +
> > + mutex_unlock(&data->lock);
> > + break;
> > + }
> > + }
> > +
> > + return 0;
> > +}
> > +
> > +static int ltr390_write_raw(struct iio_dev *indio_dev, struct iio_chan_spec const *chan,
> > + int val, int val2, long mask)
> > +{
> > + struct ltr390_data *data = iio_priv(indio_dev);
> > + int ret;
> > +
> > + switch (mask) {
> > + case IIO_CHAN_INFO_SCALE:
> > + if (val2 != 0)
> > + ret = -EINVAL;
> > +
> > + ret = ltr390_set_gain(data, val);
> > + break;
> > +
> > + case IIO_CHAN_INFO_INT_TIME:
> > + if (val2 != 0)
> > + ret = -EINVAL;
> > +
> > + ret = ltr390_set_int_time(data, val);
> > + break;
> > +
> > + default:
> > + ret = -EINVAL;
> > + }
> > +
> > + return ret;
> Use early returns.
>
> > +}
> > +
> > +static const struct iio_info ltr390_info = {
> > + .attrs = &ltr390_attribute_group,
> > + .read_raw = ltr390_read_raw,
> > + .write_raw = ltr390_write_raw,
> > };
> >
> > static int ltr390_probe(struct i2c_client *client)
> > @@ -139,11 +352,18 @@ static int ltr390_probe(struct i2c_client *client)
> > "regmap initialization failed\n");
> >
> > data->client = client;
> > + /* default value of int time from pg: 15 of the datasheet */
> I'd spell out integration in the comment.
>
> > + data->int_time_us = 100000;
> > + /* default value of gain from pg: 16 of the datasheet */
> > + data->gain = 3;
> > + /* default mode for ltr390 is ALS mode */
> > + data->mode = LTR390_SET_ALS_MODE;
> > +
> > mutex_init(&data->lock);
> >
> > indio_dev->info = &ltr390_info;
> > - indio_dev->channels = &ltr390_channel;
> > - indio_dev->num_channels = 1;
> > + indio_dev->channels = ltr390_channels;
> > + indio_dev->num_channels = ARRAY_SIZE(ltr390_channels);
> > indio_dev->name = "ltr390";
> >
> > ret = regmap_read(data->regmap, LTR390_PART_ID, &part_number);
> > @@ -161,8 +381,7 @@ static int ltr390_probe(struct i2c_client *client)
> > /* Wait for the registers to reset before proceeding */
> > usleep_range(1000, 2000);
> >
> > - ret = regmap_set_bits(data->regmap, LTR390_MAIN_CTRL,
> > - LTR390_SENSOR_ENABLE | LTR390_UVS_MODE);
> > + ret = regmap_set_bits(data->regmap, LTR390_MAIN_CTRL, LTR390_SENSOR_ENABLE);
> > if (ret)
> > return dev_err_probe(dev, ret, "failed to enable the sensor\n");
> >
> > @@ -189,6 +408,7 @@ static struct i2c_driver ltr390_driver = {
> > .probe = ltr390_probe,
> > .id_table = ltr390_id,
> > };
> > +
> Lack of space is intentional to keep the macro closely coupled to what
> it applies to.
>
> > module_i2c_driver(ltr390_driver);
> >
> > MODULE_AUTHOR("Anshul Dalal <anshulusr@xxxxxxxxx>");
>
ACK. Will do the necessary changes and send V2 after splitting it into
4 patches (replying again because I missed replying with CC last time)