* [PATCH 0/4] iio: light: tsl2591: fix lux calculation and precision, add I2C ID table, report saturation
@ 2026-10-13 14:39 Jan Remes
2026-10-13 14:39 ` [PATCH 1/4] iio: light: tsl2591: don't divide by zero Jan Remes
` (3 more replies)
0 siblings, 4 replies; 8+ messages in thread
From: Jan Remes @ 2026-10-13 14:39 UTC (permalink / raw)
To: Jonathan Cameron
Cc: David Lechner, Nuno Sá,
Andy Shevchenko, Joe Sandom, linux-iio, linux-kernel, Jan Remes
From: Jan Remes <jan.remes@mycroftmind.com>
This series fixes several issues found while using the TSL2591 in
company products (Raspberry Pi CM5) and when testing (x86_64 laptops
with MCP2221 USB-I2C bridge).
The original field report found that the illumination value has always
been 0.0 (caused by division by zero on ARM).
Patch 1 fixes divisions by zero in the lux calculation, which occur with
LOW gain and short integration times, or in darkness when CH0 = 0.
Patch 2 adds an i2c_device_id table so the driver can be probed on systems
without CONFIG_OF.
Patch 3 makes the driver return an error instead of a bogus value when the
sensor is saturated.
Patch 4 improves the formula for calculating lux values to make the
returned value more accurate.
Patches 3 and 4 change existing behavior, I am OK with splitting the
series into two, if that makes things better.
AI assistance: Claude Code (Claude Opus 5.5) was used for research on
sensor behavior, datasheets and Adafruit library, to compare results
produced by old and new formula, to draft some code for patches 1 and 4.
I reviewed all of it and tested it on x86_64.
Jan Remes (4):
iio: light: tsl2591: don't divide by zero
iio: light: tsl2591: add I2C device ID table
iio: light: tsl2591: report saturation
iio: light: tsl2591: improve lux calculation precision
drivers/iio/light/tsl2591.c | 58 +++++++++++++++++++++++++++----------
1 file changed, 42 insertions(+), 16 deletions(-)
base-commit: a22dc7ebb21a6bd4c8b63bc2c1dd46a24945aae7
--
2.53.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH 1/4] iio: light: tsl2591: don't divide by zero
2026-10-13 14:39 [PATCH 0/4] iio: light: tsl2591: fix lux calculation and precision, add I2C ID table, report saturation Jan Remes
@ 2026-10-13 14:39 ` Jan Remes
2026-10-13 18:09 ` Andy Shevchenko
2026-10-13 14:39 ` [PATCH 2/4] iio: light: tsl2591: add I2C device ID table Jan Remes
` (2 subsequent siblings)
3 siblings, 1 reply; 8+ messages in thread
From: Jan Remes @ 2026-10-13 14:39 UTC (permalink / raw)
To: Jonathan Cameron
Cc: David Lechner, Nuno Sá,
Andy Shevchenko, Joe Sandom, linux-iio, linux-kernel, Jan Remes,
stable, Martin Triska
From: Jan Remes <jan.remes@mycroftmind.com>
The TSL2591 has two raw light channels - CH0 and CH1. The algorithm for
calculating illumination from the raw channels requires division by CH0
(which can be zero) and by "counts per lux" (CPL) value which is also zero
for (gain = LOW, integration time <= 400 ms).
Furthermore, the algorithm assumes CH0 > CH1, which can be violated in
almost complete darkness.
As a result, reading from 'in_illuminance_input' results in
division-by-zero whenever CH0 is 0 (complete darkness) or whenever the
parameters result in CPL being zero. This results in kernel oops on
x86_64 and in reading the value as 0 on arm64.
Fix that by:
- reporting CH0 <= CH1 as zero (catches also CH0 = 0)
- updating the lux algorithm to only do the division at the end
Fixes: 2335f0d7c790 ("iio: light: Added AMS tsl2591 driver implementation")
Cc: stable@vger.kernel.org
Reported-by: Martin Triska <martin.triska@mycroftmind.com>
Assisted-by: LLM
Signed-off-by: Jan Remes <jan.remes@mycroftmind.com>
---
drivers/iio/light/tsl2591.c | 32 ++++++++++++++++++++++----------
1 file changed, 22 insertions(+), 10 deletions(-)
diff --git a/drivers/iio/light/tsl2591.c b/drivers/iio/light/tsl2591.c
index ef3ed9635a1e..f56d31404c25 100644
--- a/drivers/iio/light/tsl2591.c
+++ b/drivers/iio/light/tsl2591.c
@@ -16,6 +16,7 @@
#include <linux/interrupt.h>
#include <linux/iopoll.h>
#include <linux/kernel.h>
+#include <linux/math64.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/pm_runtime.h>
@@ -362,6 +363,10 @@ static int tsl2591_wait_adc_complete(struct tsl2591_chip *chip)
* Scale values to get more representative value of lux i.e.
* lux = ((C0DATA - C1DATA) * (1000 - ((C1DATA * 1000) / C0DATA))) / CPL
*
+ * CPL is not computed on its own: at 1x gain it is below 1 for integration
+ * times under 500 ms, so the division by LUX DF is folded into the final
+ * division instead.
+ *
* Channel 0 = IR + Visible
* Channel 1 = IR only
*/
@@ -373,8 +378,10 @@ static int tsl2591_read_channel_data(struct iio_dev *indio_dev,
struct tsl2591_als_settings *settings = &chip->als_settings;
struct i2c_client *client = chip->client;
u8 als_data[TSL2591_NUM_DATA_REGISTERS];
- int counts_per_lux, int_time_fval, gain_multi, lux;
+ int int_time_fval, gain_multi;
u16 als_ch0, als_ch1;
+ u64 lux;
+ u32 rem;
int ret;
ret = tsl2591_wait_adc_complete(chip);
@@ -410,23 +417,28 @@ static int tsl2591_read_channel_data(struct iio_dev *indio_dev,
return gain_multi;
}
- int_time_fval = TSL2591_FVAL_TO_MSEC(settings->als_int_time);
- /* Calculate counts per lux value */
- counts_per_lux = (int_time_fval * gain_multi) / TSL2591_LUX_COEFFICIENT;
+ /* No visible light, this also catches C0DATA == 0 */
+ if (als_ch0 <= als_ch1) {
+ *val = 0;
+ *val2 = 0;
+ break;
+ }
- dev_dbg(&client->dev, "Counts Per Lux: %d\n", counts_per_lux);
+ int_time_fval = TSL2591_FVAL_TO_MSEC(settings->als_int_time);
/* Calculate lux value */
- lux = ((als_ch0 - als_ch1) *
- (1000 - ((als_ch1 * 1000) / als_ch0))) / counts_per_lux;
+ lux = (u64)(als_ch0 - als_ch1) *
+ (1000 - ((als_ch1 * 1000) / als_ch0)) *
+ TSL2591_LUX_COEFFICIENT;
+ lux = div_u64(lux, int_time_fval * gain_multi);
- dev_dbg(&client->dev, "Raw lux calculation: %d\n", lux);
+ dev_dbg(&client->dev, "Raw lux calculation: %llu\n", lux);
/* Divide by 1000 to get real lux value before scaling */
- *val = lux / 1000;
+ *val = div_u64_rem(lux, 1000, &rem);
/* Get the decimal part of lux reading */
- *val2 = (lux - (*val * 1000)) * 1000;
+ *val2 = rem * 1000;
break;
default:
--
2.53.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH 2/4] iio: light: tsl2591: add I2C device ID table
2026-10-13 14:39 [PATCH 0/4] iio: light: tsl2591: fix lux calculation and precision, add I2C ID table, report saturation Jan Remes
2026-10-13 14:39 ` [PATCH 1/4] iio: light: tsl2591: don't divide by zero Jan Remes
@ 2026-10-13 14:39 ` Jan Remes
2026-10-13 18:24 ` Andy Shevchenko
2026-10-13 14:39 ` [PATCH 3/4] iio: light: tsl2591: report saturation Jan Remes
2026-10-13 14:39 ` [PATCH 4/4] iio: light: tsl2591: improve lux calculation precision Jan Remes
3 siblings, 1 reply; 8+ messages in thread
From: Jan Remes @ 2026-10-13 14:39 UTC (permalink / raw)
To: Jonathan Cameron
Cc: David Lechner, Nuno Sá,
Andy Shevchenko, Joe Sandom, linux-iio, linux-kernel, Jan Remes
From: Jan Remes <jan.remes@mycroftmind.com>
The TSL2591 has only the device-tree matching table, so it cannot be
probed on a kernel with CONFIG_OF disabled.
Add I2C device ID table so that the sensor can be probed.
Signed-off-by: Jan Remes <jan.remes@mycroftmind.com>
---
drivers/iio/light/tsl2591.c | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/drivers/iio/light/tsl2591.c b/drivers/iio/light/tsl2591.c
index f56d31404c25..d9c94ca5aa26 100644
--- a/drivers/iio/light/tsl2591.c
+++ b/drivers/iio/light/tsl2591.c
@@ -1149,7 +1149,14 @@ static const struct of_device_id tsl2591_of_match[] = {
};
MODULE_DEVICE_TABLE(of, tsl2591_of_match);
+static const struct i2c_device_id tsl2591_id[] = {
+ { "tsl2591" },
+ { }
+};
+MODULE_DEVICE_TABLE(i2c, tsl2591_id);
+
static struct i2c_driver tsl2591_driver = {
+ .id_table = tsl2591_id,
.driver = {
.name = "tsl2591",
.pm = pm_ptr(&tsl2591_pm_ops),
--
2.53.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH 3/4] iio: light: tsl2591: report saturation
2026-10-13 14:39 [PATCH 0/4] iio: light: tsl2591: fix lux calculation and precision, add I2C ID table, report saturation Jan Remes
2026-10-13 14:39 ` [PATCH 1/4] iio: light: tsl2591: don't divide by zero Jan Remes
2026-10-13 14:39 ` [PATCH 2/4] iio: light: tsl2591: add I2C device ID table Jan Remes
@ 2026-10-13 14:39 ` Jan Remes
2026-10-13 14:39 ` [PATCH 4/4] iio: light: tsl2591: improve lux calculation precision Jan Remes
3 siblings, 0 replies; 8+ messages in thread
From: Jan Remes @ 2026-10-13 14:39 UTC (permalink / raw)
To: Jonathan Cameron
Cc: David Lechner, Nuno Sá,
Andy Shevchenko, Joe Sandom, linux-iio, linux-kernel, Jan Remes
From: Jan Remes <jan.remes@mycroftmind.com>
When the TSL2591 sensors is saturated, it reports its maximum value
(specific one for 100ms integration time, UINT16_MAX otherwise).
When either raw channel becomes saturated, return -EOVERFLOW (like
as73211 and si1133 do) instead of calculating an invalid number when
reading 'in_illuminance_input'. Keep existing behavior on raw channels.
Note: Datasheet versions differ in the saturation number for 100ms. Old
2013 datasheet lists it as 37888, newer versions say 36863. Use the new
value although the hardware can report up to 37888.
Signed-off-by: Jan Remes <jan.remes@mycroftmind.com>
---
drivers/iio/light/tsl2591.c | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/drivers/iio/light/tsl2591.c b/drivers/iio/light/tsl2591.c
index d9c94ca5aa26..b780b236897c 100644
--- a/drivers/iio/light/tsl2591.c
+++ b/drivers/iio/light/tsl2591.c
@@ -139,7 +139,9 @@
/* TSL2591 maximum values */
#define TSL2591_MAX_ALS_INT_TIME_MS 600
+/* Maximal values per datasheet [v3-00] 2023-Feb-08 */
#define TSL2591_ALS_MAX_VALUE (BIT(16) - 1)
+#define TSL2591_ALS_MAX_VALUE_100MS 36863
/*
* LUX calculations;
@@ -379,7 +381,7 @@ static int tsl2591_read_channel_data(struct iio_dev *indio_dev,
struct i2c_client *client = chip->client;
u8 als_data[TSL2591_NUM_DATA_REGISTERS];
int int_time_fval, gain_multi;
- u16 als_ch0, als_ch1;
+ u16 als_ch0, als_ch1, als_max;
u64 lux;
u32 rem;
int ret;
@@ -417,6 +419,14 @@ static int tsl2591_read_channel_data(struct iio_dev *indio_dev,
return gain_multi;
}
+ /* Check for overflow; special case for 100ms integration time */
+ als_max = TSL2591_ALS_MAX_VALUE;
+ if (settings->als_int_time == TSL2591_CTRL_ALS_INTEGRATION_100MS)
+ als_max = TSL2591_ALS_MAX_VALUE_100MS;
+
+ if (als_ch0 >= als_max || als_ch1 >= als_max)
+ return -EOVERFLOW;
+
/* No visible light, this also catches C0DATA == 0 */
if (als_ch0 <= als_ch1) {
*val = 0;
--
2.53.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH 4/4] iio: light: tsl2591: improve lux calculation precision
2026-10-13 14:39 [PATCH 0/4] iio: light: tsl2591: fix lux calculation and precision, add I2C ID table, report saturation Jan Remes
` (2 preceding siblings ...)
2026-10-13 14:39 ` [PATCH 3/4] iio: light: tsl2591: report saturation Jan Remes
@ 2026-10-13 14:39 ` Jan Remes
2026-10-13 18:27 ` Andy Shevchenko
3 siblings, 1 reply; 8+ messages in thread
From: Jan Remes @ 2026-10-13 14:39 UTC (permalink / raw)
To: Jonathan Cameron
Cc: David Lechner, Nuno Sá,
Andy Shevchenko, Joe Sandom, linux-iio, linux-kernel, Jan Remes
From: Jan Remes <jan.remes@mycroftmind.com>
The lux calculation computes the ratio CH1 / CH0 as a truncated integer
(CH1 * 1000 / CH0) before using it. When CH1 is close to CH0, the
truncation makes the result off by up to ~35%, e.g. CH0 = 56097,
CH1 = 55973 at LOW gain and 100ms gives 1.517 lux instead of 1.118 lux.
The formula (CH0 - CH1) * (1 - CH1 / CH0) / CPL is equal to
(CH0 - CH1)^2 * DF / (CH0 * ATIME_ms * AGAIN)
which needs no intermediate ratio. Compute it like that, with a single
division at the end.
Also report the result in micro-lux instead of milli-lux. At MAX gain
and 600ms one count is well below 1 mlx, so the milli-lux resolution
throws away information. The numerator still fits into u64.
Assisted-by: LLM
Signed-off-by: Jan Remes <jan.remes@mycroftmind.com>
---
drivers/iio/light/tsl2591.c | 27 ++++++++++++---------------
1 file changed, 12 insertions(+), 15 deletions(-)
diff --git a/drivers/iio/light/tsl2591.c b/drivers/iio/light/tsl2591.c
index b780b236897c..82085bd06850 100644
--- a/drivers/iio/light/tsl2591.c
+++ b/drivers/iio/light/tsl2591.c
@@ -21,6 +21,7 @@
#include <linux/mutex.h>
#include <linux/pm_runtime.h>
#include <linux/sysfs.h>
+#include <linux/units.h>
#include <linux/unaligned.h>
@@ -362,12 +363,12 @@ static int tsl2591_wait_adc_complete(struct tsl2591_chip *chip)
* Counts Per Lux (CPL) = (ATIME_ms * AGAIN) / LUX DF
* lux = ((C0DATA - C1DATA) * (1 - (C1DATA / C0DATA))) / CPL
*
- * Scale values to get more representative value of lux i.e.
- * lux = ((C0DATA - C1DATA) * (1000 - ((C1DATA * 1000) / C0DATA))) / CPL
+ * Since (C0DATA - C1DATA) * (1 - (C1DATA / C0DATA)) equals
+ * (C0DATA - C1DATA)^2 / C0DATA, this is computed in micro-lux as
+ * lux = ((C0DATA - C1DATA)^2 * LUX DF * 10^6) / (C0DATA * ATIME_ms * AGAIN)
*
* CPL is not computed on its own: at 1x gain it is below 1 for integration
- * times under 500 ms, so the division by LUX DF is folded into the final
- * division instead.
+ * times under 500 ms, so all divisions are done at the end.
*
* Channel 0 = IR + Visible
* Channel 1 = IR only
@@ -383,7 +384,7 @@ static int tsl2591_read_channel_data(struct iio_dev *indio_dev,
int int_time_fval, gain_multi;
u16 als_ch0, als_ch1, als_max;
u64 lux;
- u32 rem;
+ u32 diff, rem;
int ret;
ret = tsl2591_wait_adc_complete(chip);
@@ -436,19 +437,15 @@ static int tsl2591_read_channel_data(struct iio_dev *indio_dev,
int_time_fval = TSL2591_FVAL_TO_MSEC(settings->als_int_time);
- /* Calculate lux value */
- lux = (u64)(als_ch0 - als_ch1) *
- (1000 - ((als_ch1 * 1000) / als_ch0)) *
- TSL2591_LUX_COEFFICIENT;
- lux = div_u64(lux, int_time_fval * gain_multi);
+ /* Calculate lux value in micro-lux */
+ diff = als_ch0 - als_ch1;
+ lux = (u64)diff * diff * TSL2591_LUX_COEFFICIENT * MICRO;
+ lux = div64_u64(lux, (u64)als_ch0 * int_time_fval * gain_multi);
dev_dbg(&client->dev, "Raw lux calculation: %llu\n", lux);
- /* Divide by 1000 to get real lux value before scaling */
- *val = div_u64_rem(lux, 1000, &rem);
-
- /* Get the decimal part of lux reading */
- *val2 = rem * 1000;
+ *val = div_u64_rem(lux, MICRO, &rem);
+ *val2 = rem;
break;
default:
--
2.53.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH 1/4] iio: light: tsl2591: don't divide by zero
2026-10-13 14:39 ` [PATCH 1/4] iio: light: tsl2591: don't divide by zero Jan Remes
@ 2026-10-13 18:09 ` Andy Shevchenko
0 siblings, 0 replies; 8+ messages in thread
From: Andy Shevchenko @ 2026-10-13 18:09 UTC (permalink / raw)
To: Jan Remes
Cc: Jonathan Cameron, David Lechner, Nuno Sá,
Andy Shevchenko, Joe Sandom, linux-iio, linux-kernel, Jan Remes,
stable, Martin Triska
On Tue, Oct 13, 2026 at 04:39:40PM +0200, Jan Remes wrote:
> The TSL2591 has two raw light channels - CH0 and CH1. The algorithm for
> calculating illumination from the raw channels requires division by CH0
> (which can be zero) and by "counts per lux" (CPL) value which is also zero
> for (gain = LOW, integration time <= 400 ms).
>
> Furthermore, the algorithm assumes CH0 > CH1, which can be violated in
> almost complete darkness.
>
> As a result, reading from 'in_illuminance_input' results in
> division-by-zero whenever CH0 is 0 (complete darkness) or whenever the
> parameters result in CPL being zero. This results in kernel oops on
> x86_64 and in reading the value as 0 on arm64.
>
> Fix that by:
> - reporting CH0 <= CH1 as zero (catches also CH0 = 0)
> - updating the lux algorithm to only do the division at the end
>
> Fixes: 2335f0d7c790 ("iio: light: Added AMS tsl2591 driver implementation")
> Cc: stable@vger.kernel.org
> Reported-by: Martin Triska <martin.triska@mycroftmind.com>
How was it reported? Offline? Off the list?
Otherwise we have Closes tag to point to the report
--
With Best Regards,
Andy Shevchenko
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH 2/4] iio: light: tsl2591: add I2C device ID table
2026-10-13 14:39 ` [PATCH 2/4] iio: light: tsl2591: add I2C device ID table Jan Remes
@ 2026-10-13 18:24 ` Andy Shevchenko
0 siblings, 0 replies; 8+ messages in thread
From: Andy Shevchenko @ 2026-10-13 18:24 UTC (permalink / raw)
To: Jan Remes
Cc: Jonathan Cameron, David Lechner, Nuno Sá,
Andy Shevchenko, Joe Sandom, linux-iio, linux-kernel, Jan Remes
On Tue, Oct 13, 2026 at 04:39:41PM +0200, Jan Remes wrote:
> The TSL2591 has only the device-tree matching table, so it cannot be
> probed on a kernel with CONFIG_OF disabled.
>
> Add I2C device ID table so that the sensor can be probed.
...
> +static const struct i2c_device_id tsl2591_id[] = {
> + { "tsl2591" },
Use C99 initialisers.
> + { }
> +};
> +MODULE_DEVICE_TABLE(i2c, tsl2591_id);
--
With Best Regards,
Andy Shevchenko
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH 4/4] iio: light: tsl2591: improve lux calculation precision
2026-10-13 14:39 ` [PATCH 4/4] iio: light: tsl2591: improve lux calculation precision Jan Remes
@ 2026-10-13 18:27 ` Andy Shevchenko
0 siblings, 0 replies; 8+ messages in thread
From: Andy Shevchenko @ 2026-10-13 18:27 UTC (permalink / raw)
To: Jan Remes
Cc: Jonathan Cameron, David Lechner, Nuno Sá,
Andy Shevchenko, Joe Sandom, linux-iio, linux-kernel, Jan Remes
On Tue, Oct 13, 2026 at 04:39:43PM +0200, Jan Remes wrote:
> The lux calculation computes the ratio CH1 / CH0 as a truncated integer
> (CH1 * 1000 / CH0) before using it. When CH1 is close to CH0, the
> truncation makes the result off by up to ~35%, e.g. CH0 = 56097,
> CH1 = 55973 at LOW gain and 100ms gives 1.517 lux instead of 1.118 lux.
>
> The formula (CH0 - CH1) * (1 - CH1 / CH0) / CPL is equal to
>
> (CH0 - CH1)^2 * DF / (CH0 * ATIME_ms * AGAIN)
>
> which needs no intermediate ratio. Compute it like that, with a single
> division at the end.
>
> Also report the result in micro-lux instead of milli-lux. At MAX gain
> and 600ms one count is well below 1 mlx, so the milli-lux resolution
> throws away information. The numerator still fits into u64.
...
> static int tsl2591_read_channel_data(struct iio_dev *indio_dev,
> int int_time_fval, gain_multi;
> u16 als_ch0, als_ch1, als_max;
> u64 lux;
> - u32 rem;
> + u32 diff, rem;
Keep that to be in reversed xmas tree order.
> int ret;
--
With Best Regards,
Andy Shevchenko
^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-10-13 18:27 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-13 14:39 [PATCH 0/4] iio: light: tsl2591: fix lux calculation and precision, add I2C ID table, report saturation Jan Remes
2026-10-13 14:39 ` [PATCH 1/4] iio: light: tsl2591: don't divide by zero Jan Remes
2026-10-13 18:09 ` Andy Shevchenko
2026-10-13 14:39 ` [PATCH 2/4] iio: light: tsl2591: add I2C device ID table Jan Remes
2026-10-13 18:24 ` Andy Shevchenko
2026-10-13 14:39 ` [PATCH 3/4] iio: light: tsl2591: report saturation Jan Remes
2026-10-13 14:39 ` [PATCH 4/4] iio: light: tsl2591: improve lux calculation precision Jan Remes
2026-10-13 18:27 ` Andy Shevchenko
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®