mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [RFC PATCH 1/2] dt-bindings: leds: Document TI LM3643 dual LED flash driver
       [not found] <20260821083620.68324-1-rillian.grant@gmail.com>
@ 2026-08-21  8:36 ` Rillian Grant
  2026-08-21  8:36 ` [RFC PATCH 2/2] leds: flash: Add support for the " Rillian Grant
  2026-09-24 17:20 ` [PATCH 0/2] leds: flash: lm3643: standby fix and ACPI LED description Ruslan Koreev
  2 siblings, 0 replies; 5+ messages in thread
From: Rillian Grant @ 2026-08-21  8:36 UTC (permalink / raw)
  To: lee, pavel, linux-leds
  Cc: Rillian Grant, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	devicetree, linux-kernel

Document Texas Instruments LM3643 dual LED flash driver devicetree
bindings.

Signed-off-by: Rillian Grant <rillian.grant@gmail.com>
---
 .../devicetree/bindings/leds/ti,lm3643.yaml   | 126 ++++++++++++++++++
 1 file changed, 126 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/leds/ti,lm3643.yaml

diff --git a/Documentation/devicetree/bindings/leds/ti,lm3643.yaml b/Documentation/devicetree/bindings/leds/ti,lm3643.yaml
new file mode 100644
index 000000000000..18b08a79512f
--- /dev/null
+++ b/Documentation/devicetree/bindings/leds/ti,lm3643.yaml
@@ -0,0 +1,126 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/leds/ti,lm3643.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Texas Instruments LM3643 1500mA dual LED Flash Driver
+
+maintainers:
+  - Rillian Grant <rillian.grant@gmail.com>
+
+description: |
+  The LM3643 is a dual LED flash driver utilizing a synchronous boost converter
+  to provide power to two current sources.
+
+  The dual high-side current sources can be tied together to provide flash
+  currents up to 1.5A through a single LED.
+
+  Datasheet: https://www.ti.com/lit/ds/symlink/lm3643.pdf
+
+properties:
+  compatible:
+    enum:
+      - ti,lm3643
+
+  reg:
+    maxItems: 1
+
+  "#address-cells":
+    const: 1
+
+  "#size-cells":
+    const: 0
+
+patternProperties:
+  "^led@[0-1]$":
+    type: object
+    $ref: common.yaml#
+    unevaluatedProperties: false
+
+    properties:
+      reg:
+        description: Index of the LED.
+        minimum: 0
+        maximum: 1
+
+      led-sources:
+        minItems: 1
+        maxItems: 2
+        items:
+          minimum: 0
+          maximum: 1
+
+      flash-max-microamp:
+        description: |
+          Maximum flash current through this LED. Each high-side current source
+          can provide up to 1.5A. The maximum current the boost can deliver is
+          1.5A across both sources.
+
+          Valid values for an LED connected to one source:
+            10900 - 1500000, step by 11725 (rounded down)
+          Valid values for an LED connected to both sources:
+            21800 - 1500000, step by 23450 (rounded down)
+
+        minimum: 10900
+        maximum: 1500000
+
+      led-max-microamp:
+        description: |
+          Maximum torch current through this LED. In torch mode each high-side
+          current source can provide up to 179mA.
+
+          Valid values for an LED connected to one source:
+            977 - 179000, step by 1400 (rounded down)
+          Valid values for an LED connected to both sources:
+            1954 - 358000, step by 2800 (rounded down)
+
+        minimum: 977
+        maximum: 358000
+
+      flash-max-timeout-us:
+        description: |
+          The chip supports timeouts from 10ms to 100ms in increments of 10ms
+          and from 100ms to 400ms in increments of 50ms. The provided value
+          will be rounded down to a supported value.
+
+        minimum: 10000
+        maximum: 400000
+
+    required:
+      - reg
+      - led-sources
+
+required:
+  - compatible
+  - reg
+  - "#address-cells"
+  - "#size-cells"
+
+additionalProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/leds/common.h>
+
+    i2c {
+        #address-cells = <1>;
+        #size-cells = <0>;
+
+        flash-led-controller@63 {
+            compatible = "ti,lm3643";
+            reg = <0x63>;
+            #address-cells = <1>;
+            #size-cells = <0>;
+
+            led@0 {
+                reg = <0>;
+                led-sources = <0>, <1>;
+                function = LED_FUNCTION_FLASH;
+                color = <LED_COLOR_ID_IR>;
+                flash-max-microamp = <750000>;
+                led-max-microamp = <180000>;
+                flash-max-timeout-us = <150000>;
+            };
+        };
+    };
-- 
2.55.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

* [RFC PATCH 2/2] leds: flash: Add support for the TI LM3643 dual LED flash driver
       [not found] <20260821083620.68324-1-rillian.grant@gmail.com>
  2026-08-21  8:36 ` [RFC PATCH 1/2] dt-bindings: leds: Document TI LM3643 dual LED flash driver Rillian Grant
@ 2026-08-21  8:36 ` Rillian Grant
  2026-09-24 17:20 ` [PATCH 0/2] leds: flash: lm3643: standby fix and ACPI LED description Ruslan Koreev
  2 siblings, 0 replies; 5+ messages in thread
From: Rillian Grant @ 2026-08-21  8:36 UTC (permalink / raw)
  To: lee, pavel, linux-leds; +Cc: Rillian Grant, linux-kernel

The LM3643 is an I2C dual LED flash driver. Its two high-side current
sources share a synchronous boost converter and each supports torch and
flash modes with programmable current and flash timeout alongside fault
reporting.

It also supports connecting both sources together to supply one LED with
current up to the boost converter's 1.5A maximum. In that case the current
will be split between each source symmetrically and the sources will be
enabled and disabled at the same time.

As strobing is toggled chip-wide the driver returns -EBUSY when a sibling
LED is active.

Flash timeouts do not have a constant step. The driver advertises the most
granular step and rounds down to the nearest supported value.

Register behavior was taken from the datasheet (SNVS967A).

Signed-off-by: Rillian Grant <rillian.grant@gmail.com>
---
 MAINTAINERS                      |   7 +
 drivers/leds/flash/Kconfig       |  12 +
 drivers/leds/flash/Makefile      |   1 +
 drivers/leds/flash/leds-lm3643.c | 775 +++++++++++++++++++++++++++++++
 4 files changed, 795 insertions(+)
 create mode 100644 drivers/leds/flash/leds-lm3643.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 040df7bfb24f..5aa66c3f3237 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -26791,6 +26791,13 @@ F:	include/linux/dma/k3-udma-glue.h
 F:	include/linux/dma/ti-cppi5.h
 X:	drivers/dma/ti/cppi41.c
 
+TEXAS INSTRUMENTS LM3643 LED FLASH DRIVER
+M:	Rillian Grant <rillian.grant@gmail.com>
+L:	linux-leds@vger.kernel.org
+S:	Maintained
+F:	Documentation/devicetree/bindings/leds/ti,lm3643.yaml
+F:	drivers/leds/flash/leds-lm3643.c
+
 TEXAS INSTRUMENTS TPS25990 HARDWARE MONITOR DRIVER
 M:	Jerome Brunet <jbrunet@baylibre.com>
 L:	linux-hwmon@vger.kernel.org
diff --git a/drivers/leds/flash/Kconfig b/drivers/leds/flash/Kconfig
index 9b6dd3f1ffe8..0f0a60a902fc 100644
--- a/drivers/leds/flash/Kconfig
+++ b/drivers/leds/flash/Kconfig
@@ -39,6 +39,18 @@ config LEDS_LM3601X
 	  This option enables support for the TI LM3601x family
 	  of flash, torch and indicator classes.
 
+config LEDS_LM3643
+	tristate "LED support for LM3643 dual LED flash driver"
+	depends on I2C
+	depends on V4L2_FLASH_LED_CLASS || !V4L2_FLASH_LED_CLASS
+	select REGMAP_I2C
+	help
+	  This option enables support for the Texas Instruments LM3643 dual
+	  LED flash driver.
+
+	  This driver can also be built as a module. If so, the module
+	  will be called "leds-lm3643".
+
 config LEDS_MAX77693
 	tristate "LED support for MAX77693 Flash"
 	depends on V4L2_FLASH_LED_CLASS || !V4L2_FLASH_LED_CLASS
diff --git a/drivers/leds/flash/Makefile b/drivers/leds/flash/Makefile
index 44e6c1b4beb3..e2e885e02123 100644
--- a/drivers/leds/flash/Makefile
+++ b/drivers/leds/flash/Makefile
@@ -6,6 +6,7 @@ obj-$(CONFIG_LEDS_AAT1290)	+= leds-aat1290.o
 obj-$(CONFIG_LEDS_AS3645A)	+= leds-as3645a.o
 obj-$(CONFIG_LEDS_KTD2692)	+= leds-ktd2692.o
 obj-$(CONFIG_LEDS_LM3601X)	+= leds-lm3601x.o
+obj-$(CONFIG_LEDS_LM3643)	+= leds-lm3643.o
 obj-$(CONFIG_LEDS_MAX77693)	+= leds-max77693.o
 obj-$(CONFIG_LEDS_QCOM_FLASH)	+= leds-qcom-flash.o
 obj-$(CONFIG_LEDS_RT4505)	+= leds-rt4505.o
diff --git a/drivers/leds/flash/leds-lm3643.c b/drivers/leds/flash/leds-lm3643.c
new file mode 100644
index 000000000000..48e164261c98
--- /dev/null
+++ b/drivers/leds/flash/leds-lm3643.c
@@ -0,0 +1,775 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Texas Instruments LM3643(A) Synchronous Boost Dual LED Flash Driver
+ *
+ * Copyright 2026 Rillian Grant <rillian.grant@gmail.com>
+ */
+
+#include <linux/acpi.h>
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/cleanup.h>
+#include <linux/i2c.h>
+#include <linux/led-class-flash.h>
+#include <linux/leds.h>
+#include <linux/minmax.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+#include <media/v4l2-flash-led-class.h>
+
+#define LM3643_NUM_CHANNELS 2
+#define LM3643_CHAN_JOINT LM3643_NUM_CHANNELS
+
+#define LM3643_REG_ENABLE 0x01
+#define LM3643_REG_FLASH_BR_LED1 0x03
+#define LM3643_REG_FLASH_BR_LED2 0x04
+#define LM3643_REG_TORCH_BR_LED1 0x05
+#define LM3643_REG_TORCH_BR_LED2 0x06
+#define LM3643_REG_CONFIG 0x08
+#define LM3643_REG_FLAGS1 0x0A
+#define LM3643_REG_FLAGS2 0x0B
+#define LM3643_REG_DEV_ID 0x0C
+
+#define LM3643_ENABLE_LED_MASK GENMASK(1, 0)
+#define LM3643_ENABLE_LED1 BIT(0)
+#define LM3643_ENABLE_LED2 BIT(1)
+
+#define LM3643_MODE_MASK GENMASK(3, 2)
+#define LM3643_MODE_STANDBY 0x00
+#define LM3643_MODE_TORCH FIELD_PREP(LM3643_MODE_MASK, 0x2)
+#define LM3643_MODE_FLASH FIELD_PREP(LM3643_MODE_MASK, 0x3)
+
+#define LM3643_TORCH_BR_MASK GENMASK(6, 0)
+#define LM3643_TORCH_BR_LED2_OVERRIDE BIT(7)
+#define LM3643_TORCH_BR_CODE_RESET 0x3F
+#define LM3643_TORCH_BR_CODE_MAX 0x7F
+
+/* Torch: microamps = (code x 1.4) + 0.977 */
+#define LM3643_TORCH_BR_UA_OFFSET 977
+#define LM3643_TORCH_BR_UA_STEP 1400
+#define LM3643_TORCH_BR_UA_TO_CODE(ua) \
+	(((ua) - LM3643_TORCH_BR_UA_OFFSET) / LM3643_TORCH_BR_UA_STEP)
+#define LM3643_TORCH_BR_CODE_TO_UA(code) \
+	(((code) * LM3643_TORCH_BR_UA_STEP) + LM3643_TORCH_BR_UA_OFFSET)
+#define LM3643_TORCH_BR_UA_MIN LM3643_TORCH_BR_CODE_TO_UA(0)
+
+#define LM3643_FLASH_BR_MASK GENMASK(6, 0)
+#define LM3643_FLASH_BR_LED2_OVERRIDE BIT(7)
+#define LM3643_FLASH_BR_CODE_RESET 0x3F
+#define LM3643_FLASH_BR_CODE_MAX 0x7F
+
+/* Flash: microamps = (code x 11.725) + 10.9 */
+#define LM3643_FLASH_BR_UA_OFFSET 10900
+#define LM3643_FLASH_BR_UA_STEP 11725
+#define LM3643_FLASH_BR_UA_TO_CODE(ua) \
+	(((ua) - LM3643_FLASH_BR_UA_OFFSET) / LM3643_FLASH_BR_UA_STEP)
+#define LM3643_FLASH_BR_CODE_TO_UA(code) \
+	(((code) * LM3643_FLASH_BR_UA_STEP) + LM3643_FLASH_BR_UA_OFFSET)
+#define LM3643_FLASH_BR_UA_MIN LM3643_FLASH_BR_CODE_TO_UA(0)
+/* Maximum current the chip can produce across both sources */
+#define LM3643_FLASH_BR_UA_TOTAL_MAX 1500000
+
+/* CDEV brightness values are 1-indexed for use by led_classdev */
+#define LM3643_TORCH_BR_CODE_TO_CDEV(code) ((code) + 1)
+#define LM3643_TORCH_BR_CDEV_TO_CODE(brightness) ((brightness) - 1)
+#define LM3643_TORCH_BR_CDEV_MAX LM3643_TORCH_BR_CODE_TO_CDEV(LM3643_TORCH_BR_CODE_MAX)
+#define LM3643_TORCH_BR_UA_TO_CDEV(ua) \
+	  LM3643_TORCH_BR_CODE_TO_CDEV(LM3643_TORCH_BR_UA_TO_CODE(ua))
+#define LM3643_TORCH_BR_CDEV_TO_UA(brightness) \
+	  LM3643_TORCH_BR_CODE_TO_UA(LM3643_TORCH_BR_CDEV_TO_CODE(brightness))
+
+#define LM3643_CONFIG_FLASH_TIMEOUT_MASK GENMASK(3, 0)
+#define LM3643_CONFIG_FLASH_TIMEOUT_RESET 0xA
+
+#define LM3643_TIMEOUT_US_MIN 10000
+#define LM3643_TIMEOUT_US_STEP 10000
+#define LM3643_TIMEOUT_US_MAX 400000
+
+#define LM3643_FLAGS1_MASK GENMASK(6, 0)
+#define LM3643_FLAGS1_FAULT_TIMEOUT BIT(0)
+#define LM3643_FLAGS1_FAULT_UVLO BIT(1)
+#define LM3643_FLAGS1_FAULT_TSD BIT(2)
+#define LM3643_FLAGS1_FAULT_CURRENT_LIMIT BIT(3)
+#define LM3643_FLAGS1_FAULT_VLED2_SHORT BIT(4)
+#define LM3643_FLAGS1_FAULT_VLED1_SHORT BIT(5)
+#define LM3643_FLAGS1_FAULT_VOUT_SHORT BIT(6)
+#define LM3643_FLAGS1_LED1_FAULTS (LM3643_FLAGS1_MASK & ~LM3643_FLAGS1_FAULT_VLED2_SHORT)
+#define LM3643_FLAGS1_LED2_FAULTS (LM3643_FLAGS1_MASK & ~LM3643_FLAGS1_FAULT_VLED1_SHORT)
+
+#define LM3643_FLAGS2_MASK GENMASK(2, 0)
+#define LM3643_FLAGS2_FAULT_TEMP BIT(0)
+#define LM3643_FLAGS2_FAULT_OVP BIT(1)
+#define LM3643_FLAGS2_FAULT_IVFM_TRIP BIT(2)
+
+#define LM3643_FAULTS_ALL ( \
+	LED_FAULT_TIMEOUT \
+	| LED_FAULT_UNDER_VOLTAGE \
+	| LED_FAULT_OVER_TEMPERATURE \
+	| LED_FAULT_OVER_CURRENT \
+	| LED_FAULT_SHORT_CIRCUIT \
+	| LED_FAULT_LED_OVER_TEMPERATURE \
+	| LED_FAULT_OVER_VOLTAGE \
+	| LED_FAULT_INPUT_VOLTAGE \
+)
+
+#define LM3643_DEV_ID_MASK GENMASK(5, 3)
+#define LM3643_DEV_ID 0x00
+
+struct lm3643_chan {
+	u8 enable_bit;
+	u8 torch_br_reg;
+	u8 flash_br_reg;
+	u8 flags1_faults;
+	u8 num_sources;
+};
+
+static const struct lm3643_chan lm3643_chans[LM3643_NUM_CHANNELS + 1] = {
+	{
+		.enable_bit = LM3643_ENABLE_LED1,
+		.torch_br_reg = LM3643_REG_TORCH_BR_LED1,
+		.flash_br_reg = LM3643_REG_FLASH_BR_LED1,
+		.flags1_faults = LM3643_FLAGS1_LED1_FAULTS,
+		.num_sources = 1,
+	},
+	{
+		.enable_bit = LM3643_ENABLE_LED2,
+		.torch_br_reg = LM3643_REG_TORCH_BR_LED2,
+		.flash_br_reg = LM3643_REG_FLASH_BR_LED2,
+		.flags1_faults = LM3643_FLAGS1_LED2_FAULTS,
+		.num_sources = 1,
+	},
+	[LM3643_CHAN_JOINT] = {
+		.enable_bit = LM3643_ENABLE_LED_MASK,
+		.torch_br_reg = LM3643_REG_TORCH_BR_LED1,
+		.flash_br_reg = LM3643_REG_FLASH_BR_LED1,
+		.flags1_faults = LM3643_FLAGS1_MASK,
+		.num_sources = LM3643_NUM_CHANNELS,
+	},
+};
+
+static const struct led_flash_setting lm3643_flash_br_setting = {
+	.max = LM3643_FLASH_BR_CODE_TO_UA(LM3643_FLASH_BR_CODE_MAX),
+	.min = LM3643_FLASH_BR_UA_MIN,
+	.step = LM3643_FLASH_BR_UA_STEP,
+	.val = LM3643_FLASH_BR_CODE_TO_UA(LM3643_FLASH_BR_CODE_RESET),
+};
+
+static const u32 lm3643_timeout_us[] = {
+	LM3643_TIMEOUT_US_MIN,
+	20000,
+	30000,
+	40000,
+	50000,
+	60000,
+	70000,
+	80000,
+	90000,
+	100000,
+	150000,
+	200000,
+	250000,
+	300000,
+	350000,
+	LM3643_TIMEOUT_US_MAX,
+};
+
+static_assert(ARRAY_SIZE(lm3643_timeout_us) == LM3643_CONFIG_FLASH_TIMEOUT_MASK + 1);
+
+static unsigned int lm3643_timeout_to_code(u32 timeout)
+{
+	unsigned int i;
+
+	for (i = ARRAY_SIZE(lm3643_timeout_us) - 1; i > 0; i--)
+		if (timeout >= lm3643_timeout_us[i])
+			break;
+
+	return i;
+}
+
+/* The chip's timeouts step by 10 ms up to 100 ms and then by 50 ms. */
+static const struct led_flash_setting lm3643_flash_time_setting = {
+	.max = LM3643_TIMEOUT_US_MAX,
+	.min = LM3643_TIMEOUT_US_MIN,
+	.step = LM3643_TIMEOUT_US_STEP,
+	.val = lm3643_timeout_us[LM3643_CONFIG_FLASH_TIMEOUT_RESET],
+};
+
+struct lm3643_led {
+	struct lm3643 *chip;
+	struct regmap *regmap;
+	struct led_classdev_flash flash_cdev;
+	struct v4l2_flash *v4l2_flash;
+
+	const struct lm3643_chan *chan;
+
+	u8 flags1, flags2;
+};
+
+struct lm3643 {
+	struct regmap *regmap;
+	/* Synchronizes access to enable and flag registers */
+	struct mutex lock;
+	struct lm3643_led leds[LM3643_NUM_CHANNELS];
+	unsigned int leds_active;
+};
+
+static enum led_brightness lm3643_torch_get_brightness(struct led_classdev *led_cdev)
+{
+	struct lm3643_led *led = container_of(lcdev_to_flcdev(led_cdev),
+										  struct lm3643_led,
+										  flash_cdev);
+	const struct lm3643_chan *chan = led->chan;
+	unsigned int brightness;
+	unsigned int enable;
+	int ret;
+
+	guard(mutex)(&led->chip->lock);
+
+	ret = regmap_read(led->regmap, LM3643_REG_ENABLE, &enable);
+	if (ret) {
+		dev_err(led_cdev->dev, "failed to get enable register\n");
+		return LED_OFF;
+	}
+	enable &= LM3643_MODE_MASK | chan->enable_bit;
+	if (enable != (LM3643_MODE_TORCH | chan->enable_bit))
+		return LED_OFF;
+
+	ret = regmap_read(led->regmap, chan->torch_br_reg, &brightness);
+	if (ret) {
+		dev_err(led_cdev->dev,
+			"failed to get LED brightness register 0x%02x\n",
+			chan->torch_br_reg);
+		return LED_OFF;
+	}
+	brightness &= LM3643_TORCH_BR_MASK;
+
+	return LM3643_TORCH_BR_CODE_TO_CDEV(brightness);
+}
+
+static int lm3643_torch_set_brightness(struct led_classdev *led_cdev,
+				       enum led_brightness brightness)
+{
+	struct lm3643_led *led = container_of(lcdev_to_flcdev(led_cdev),
+					      struct lm3643_led, flash_cdev);
+	const struct lm3643_chan *chan = led->chan;
+	struct lm3643 *chip = led->chip;
+	unsigned int sibling_bit, keep, enable, mode;
+	int ret;
+
+	guard(mutex)(&chip->lock);
+
+	ret = regmap_read(led->regmap, LM3643_REG_ENABLE, &enable);
+	if (ret)
+		return ret;
+	sibling_bit = LM3643_ENABLE_LED_MASK & ~chan->enable_bit;
+
+	mode = enable & LM3643_MODE_MASK;
+	if ((enable & sibling_bit) &&
+	    mode != LM3643_MODE_STANDBY &&
+	    mode != LM3643_MODE_TORCH)
+		return -EBUSY;
+
+	if (brightness == 0)
+		return regmap_update_bits(led->regmap, LM3643_REG_ENABLE,
+				chan->enable_bit, 0);
+
+	ret = regmap_update_bits(led->regmap, chan->torch_br_reg, LM3643_TORCH_BR_MASK,
+				 LM3643_TORCH_BR_CDEV_TO_CODE(brightness));
+	if (ret)
+		return ret;
+
+	keep = (enable & LM3643_MODE_MASK) == LM3643_MODE_TORCH
+		? enable & sibling_bit
+		: 0;
+
+	return regmap_update_bits(led->regmap,
+				  LM3643_REG_ENABLE,
+				  LM3643_MODE_MASK | LM3643_ENABLE_LED_MASK,
+				  LM3643_MODE_TORCH | chan->enable_bit | keep);
+}
+
+static int lm3643_flash_brightness_get(struct led_classdev_flash *fled_cdev, u32 *brightness)
+{
+	struct lm3643_led *led = container_of(fled_cdev,
+					      struct lm3643_led,
+					      flash_cdev);
+	const struct lm3643_chan *chan = led->chan;
+	int ret;
+
+	ret = regmap_read(led->regmap, chan->flash_br_reg, brightness);
+	if (ret)
+		return ret;
+
+	*brightness &= LM3643_FLASH_BR_MASK;
+	*brightness = LM3643_FLASH_BR_CODE_TO_UA(*brightness);
+	*brightness *= chan->num_sources;
+
+	return 0;
+}
+
+static int lm3643_flash_brightness_set(struct led_classdev_flash *fled_cdev, u32 brightness)
+{
+	struct lm3643_led *led = container_of(fled_cdev, struct lm3643_led, flash_cdev);
+	const struct lm3643_chan *chan = led->chan;
+
+	brightness /= chan->num_sources;
+	brightness = LM3643_FLASH_BR_UA_TO_CODE(brightness);
+
+	return regmap_update_bits(led->regmap,
+				  chan->flash_br_reg,
+				  LM3643_FLASH_BR_MASK,
+				  brightness);
+}
+
+static int lm3643_flash_timeout_set(struct led_classdev_flash *fled_cdev, u32 timeout)
+{
+	unsigned int code = lm3643_timeout_to_code(timeout);
+
+	fled_cdev->timeout.val = lm3643_timeout_us[code];
+
+	return 0;
+}
+
+static int lm3643_flash_strobe_get(struct led_classdev_flash *fled_cdev, bool *state)
+{
+	struct lm3643_led *led = container_of(fled_cdev, struct lm3643_led, flash_cdev);
+	const struct lm3643_chan *chan = led->chan;
+	unsigned int mode;
+	int ret;
+
+	ret = regmap_read(led->regmap, LM3643_REG_ENABLE, &mode);
+	if (ret)
+		return ret;
+
+	*state = (mode & (LM3643_MODE_MASK | chan->enable_bit)) ==
+		 (LM3643_MODE_FLASH | chan->enable_bit);
+
+	return 0;
+}
+
+static int lm3643_flash_strobe_set(struct led_classdev_flash *fled_cdev, bool state)
+{
+	struct lm3643_led *led = container_of(fled_cdev, struct lm3643_led, flash_cdev);
+	const struct lm3643_chan *chan = led->chan;
+	unsigned int enable_reg, sibling_bit, mode;
+	int ret;
+
+	guard(mutex)(&led->chip->lock);
+
+	ret = regmap_read(led->regmap, LM3643_REG_ENABLE, &enable_reg);
+	if (ret)
+		return ret;
+	sibling_bit = LM3643_ENABLE_LED_MASK & ~led->chan->enable_bit;
+	mode = enable_reg & LM3643_MODE_MASK;
+
+	if (!state) {
+		if (mode != LM3643_MODE_FLASH || !(enable_reg & chan->enable_bit))
+			return 0;
+
+		return regmap_update_bits(led->regmap, LM3643_REG_ENABLE,
+			LM3643_MODE_MASK, LM3643_MODE_STANDBY);
+	}
+	if ((enable_reg & sibling_bit) && mode != LM3643_MODE_STANDBY)
+		return -EBUSY;
+
+	ret = regmap_update_bits(led->regmap,
+				 LM3643_REG_CONFIG,
+				 LM3643_CONFIG_FLASH_TIMEOUT_MASK,
+				 lm3643_timeout_to_code(fled_cdev->timeout.val));
+	if (ret)
+		return ret;
+
+	return regmap_update_bits(led->regmap,
+				  LM3643_REG_ENABLE,
+				  LM3643_MODE_MASK | LM3643_ENABLE_LED_MASK,
+				  LM3643_MODE_FLASH | chan->enable_bit);
+}
+
+static unsigned int lm3643_decode_faults(u8 flags1, u8 flags2)
+{
+	unsigned int faults = 0;
+
+	if (flags1 & LM3643_FLAGS1_FAULT_TIMEOUT)
+		faults |= LED_FAULT_TIMEOUT;
+	if (flags1 & LM3643_FLAGS1_FAULT_UVLO)
+		faults |= LED_FAULT_UNDER_VOLTAGE;
+	if (flags1 & LM3643_FLAGS1_FAULT_TSD)
+		faults |= LED_FAULT_OVER_TEMPERATURE;
+	if (flags1 & LM3643_FLAGS1_FAULT_CURRENT_LIMIT)
+		faults |= LED_FAULT_OVER_CURRENT;
+
+	/*
+	 * The caller has already masked off the sibling channel's short flag,
+	 * so all three short conditions collapse to the one generic fault.
+	 */
+	if (flags1 & (LM3643_FLAGS1_FAULT_VLED1_SHORT |
+			  LM3643_FLAGS1_FAULT_VLED2_SHORT |
+			  LM3643_FLAGS1_FAULT_VOUT_SHORT))
+		faults |= LED_FAULT_SHORT_CIRCUIT;
+
+	/* TSD is the die tripping at 150C; TEMP is the external NTC at the LED. */
+	if (flags2 & LM3643_FLAGS2_FAULT_TEMP)
+		faults |= LED_FAULT_LED_OVER_TEMPERATURE;
+	if (flags2 & LM3643_FLAGS2_FAULT_OVP)
+		faults |= LED_FAULT_OVER_VOLTAGE;
+	if (flags2 & LM3643_FLAGS2_FAULT_IVFM_TRIP)
+		faults |= LED_FAULT_INPUT_VOLTAGE;
+
+	return faults;
+}
+
+static int lm3643_fault_get(struct led_classdev_flash *fled_cdev, u32 *fault)
+{
+	struct lm3643_led *led = container_of(fled_cdev, struct lm3643_led, flash_cdev);
+	struct lm3643 *chip = led->chip;
+	u32 flags1, flags2;
+	int ret;
+
+	guard(mutex)(&chip->lock);
+
+	ret = regmap_read(chip->regmap, LM3643_REG_FLAGS1, &flags1);
+	if (ret)
+		return ret;
+
+	ret = regmap_read(chip->regmap, LM3643_REG_FLAGS2, &flags2);
+	if (ret)
+		return ret;
+
+	flags1 &= LM3643_FLAGS1_MASK;
+	flags2 &= LM3643_FLAGS2_MASK;
+
+	for (int i = 0; i < LM3643_NUM_CHANNELS; i++) {
+		struct lm3643_led *sibling = &chip->leds[i];
+
+		if (!sibling->chan)
+			continue;
+
+		sibling->flags1 |= flags1 & sibling->chan->flags1_faults;
+		sibling->flags2 |= flags2;
+	}
+
+	*fault = lm3643_decode_faults(led->flags1, led->flags2);
+	led->flags1 = 0;
+	led->flags2 = 0;
+
+	return 0;
+}
+
+static const struct led_flash_ops lm3643_flash_ops = {
+	.strobe_get = lm3643_flash_strobe_get,
+	.strobe_set = lm3643_flash_strobe_set,
+	.flash_brightness_get = lm3643_flash_brightness_get,
+	.flash_brightness_set = lm3643_flash_brightness_set,
+	.timeout_set = lm3643_flash_timeout_set,
+	.fault_get = lm3643_fault_get,
+};
+
+static const struct regmap_config lm3643_regmap_config = {
+	.reg_bits = 8,
+	.val_bits = 8,
+	.max_register = 0x0D,
+	.cache_type = REGCACHE_NONE,
+};
+
+static void lm3643_v4l2_release(void *v4l2_flash)
+{
+	v4l2_flash_release(v4l2_flash);
+}
+
+static void lm3643_standby(void *regmap)
+{
+	regmap_write(regmap, LM3643_REG_ENABLE, LM3643_MODE_STANDBY);
+}
+
+static void lm3643_scale_flash_setting_microamps(struct led_flash_setting *setting, u8 num_sources)
+{
+	setting->max *= num_sources;
+	setting->min *= num_sources;
+	setting->step *= num_sources;
+	setting->val *= num_sources;
+}
+
+static struct lm3643_led *lm3643_claim_channels(struct device *dev, struct lm3643 *chip,
+						struct fwnode_handle *fwnode)
+{
+	u32 sources[LM3643_NUM_CHANNELS];
+	struct lm3643_led *led;
+	int num_sources;
+	int ret;
+
+	num_sources = fwnode_property_count_u32(fwnode, "led-sources");
+	if (num_sources < 0)
+		return ERR_PTR(dev_err_probe(dev, num_sources,
+			"failed to read led-sources property\n"));
+	if (num_sources < 1 || num_sources > LM3643_NUM_CHANNELS)
+		return ERR_PTR(dev_err_probe(dev, -EINVAL,
+			"led-sources has %d entries, expected 1 to %d\n",
+			num_sources, LM3643_NUM_CHANNELS));
+
+	ret = fwnode_property_read_u32_array(fwnode, "led-sources", sources, num_sources);
+	if (ret)
+		return ERR_PTR(dev_err_probe(dev, ret,
+			"failed to read led-sources property\n"));
+
+	for (int i = 0; i < num_sources; i++) {
+		if (sources[i] >= LM3643_NUM_CHANNELS)
+			return ERR_PTR(dev_err_probe(dev, -EINVAL,
+				"led-sources entry %u exceeds the %d current outputs\n",
+				sources[i], LM3643_NUM_CHANNELS));
+		if (chip->leds_active & BIT(sources[i]))
+			return ERR_PTR(dev_err_probe(dev, -EINVAL,
+				"current output %u claimed more than once\n", sources[i]));
+
+		chip->leds_active |= BIT(sources[i]);
+	}
+
+	if (num_sources == LM3643_NUM_CHANNELS) {
+		led = &chip->leds[0];
+		led->chan = &lm3643_chans[LM3643_CHAN_JOINT];
+	} else {
+		led = &chip->leds[sources[0]];
+		led->chan = &lm3643_chans[sources[0]];
+	}
+
+	led->chip = chip;
+	led->regmap = chip->regmap;
+
+	return led;
+}
+
+static int lm3643_apply_fw_limits(struct device *dev, struct lm3643_led *led,
+				  struct fwnode_handle *fwnode)
+{
+	struct led_classdev_flash *flash_cdev = &led->flash_cdev;
+	struct led_classdev *led_cdev = &flash_cdev->led_cdev;
+	u8 num_sources = led->chan->num_sources;
+	u32 max_us, max_torch_ua, max_flash_ua;
+
+	if (!fwnode_property_read_u32(fwnode, "flash-max-microamp", &max_flash_ua)) {
+		if (max_flash_ua < LM3643_FLASH_BR_UA_MIN * num_sources)
+			return dev_err_probe(dev, -EINVAL,
+				"flash-max-microamp %u is below the %u uA minimum\n",
+				max_flash_ua, LM3643_FLASH_BR_UA_MIN * num_sources);
+
+		/* Rounded down to a supported value. */
+		flash_cdev->brightness.max = min(flash_cdev->brightness.max, max_flash_ua);
+	}
+
+	if (!fwnode_property_read_u32(fwnode, "led-max-microamp", &max_torch_ua)) {
+		if (max_torch_ua < LM3643_TORCH_BR_UA_MIN * num_sources)
+			return dev_err_probe(dev, -EINVAL,
+				"led-max-microamp %u is below the %u uA minimum\n",
+				max_torch_ua, LM3643_TORCH_BR_UA_MIN * num_sources);
+
+		led_cdev->max_brightness = min(led_cdev->max_brightness,
+					       LM3643_TORCH_BR_UA_TO_CDEV(max_torch_ua /
+									  num_sources));
+	}
+
+	if (!fwnode_property_read_u32(fwnode, "flash-max-timeout-us", &max_us)) {
+		if (max_us < LM3643_TIMEOUT_US_MIN)
+			return dev_err_probe(dev, -EINVAL,
+				"flash-max-timeout-us %u below the %u us minimum\n",
+				max_us, LM3643_TIMEOUT_US_MIN);
+
+		flash_cdev->timeout.max = lm3643_timeout_us[lm3643_timeout_to_code(max_us)];
+	}
+
+	return 0;
+}
+
+static int lm3643_register_v4l2(struct device *dev, struct lm3643_led *led,
+				struct fwnode_handle *fwnode)
+{
+	struct led_classdev *led_cdev = &led->flash_cdev.led_cdev;
+	struct v4l2_flash_config v4l2_flash_config = {};
+
+	strscpy(v4l2_flash_config.dev_name, dev_name(led_cdev->dev),
+		sizeof(v4l2_flash_config.dev_name));
+	v4l2_flash_config.flash_faults = LM3643_FAULTS_ALL;
+	v4l2_flash_config.has_external_strobe = false;
+	v4l2_flash_config.intensity.min = LM3643_TORCH_BR_UA_MIN;
+	v4l2_flash_config.intensity.step = LM3643_TORCH_BR_UA_STEP;
+	v4l2_flash_config.intensity.max = LM3643_TORCH_BR_CDEV_TO_UA(led_cdev->max_brightness);
+	v4l2_flash_config.intensity.val =
+		min_t(u32, LM3643_TORCH_BR_CODE_TO_UA(LM3643_TORCH_BR_CODE_RESET),
+		      v4l2_flash_config.intensity.max);
+
+	lm3643_scale_flash_setting_microamps(&v4l2_flash_config.intensity, led->chan->num_sources);
+
+	led->v4l2_flash = v4l2_flash_init(dev, fwnode, &led->flash_cdev, NULL,
+					  &v4l2_flash_config);
+	if (IS_ERR(led->v4l2_flash))
+		return dev_err_probe(dev,
+				     PTR_ERR(led->v4l2_flash),
+				     "failed to register v4l2 flash\n");
+
+	return devm_add_action_or_reset(dev, lm3643_v4l2_release, led->v4l2_flash);
+}
+
+static int lm3643_register_led(struct device *dev, struct lm3643 *chip,
+			       struct fwnode_handle *fwnode)
+{
+	struct led_init_data init_data = { .fwnode = fwnode };
+	struct led_classdev_flash *flash_cdev;
+	struct led_classdev *led_cdev;
+	struct lm3643_led *led;
+	u32 torch_max_brightness;
+	int ret;
+
+	led = lm3643_claim_channels(dev, chip, fwnode);
+	if (IS_ERR(led))
+		return PTR_ERR(led);
+
+	flash_cdev = &led->flash_cdev;
+	led_cdev = &flash_cdev->led_cdev;
+
+	if (led->chan->num_sources == LM3643_NUM_CHANNELS) {
+		/* Bit 7 of the LED1 brightness registers override LED2 with the same values. */
+		ret = regmap_update_bits(chip->regmap, LM3643_REG_TORCH_BR_LED1,
+					 LM3643_TORCH_BR_LED2_OVERRIDE,
+					 LM3643_TORCH_BR_LED2_OVERRIDE);
+		if (ret)
+			return ret;
+
+		ret = regmap_update_bits(chip->regmap, LM3643_REG_FLASH_BR_LED1,
+					 LM3643_FLASH_BR_LED2_OVERRIDE,
+					 LM3643_FLASH_BR_LED2_OVERRIDE);
+		if (ret)
+			return ret;
+	}
+
+	flash_cdev->brightness = lm3643_flash_br_setting;
+	lm3643_scale_flash_setting_microamps(&flash_cdev->brightness, led->chan->num_sources);
+	flash_cdev->timeout = lm3643_flash_time_setting;
+	flash_cdev->ops = &lm3643_flash_ops;
+
+	led_cdev->flags |= LED_DEV_CAP_FLASH;
+	led_cdev->max_brightness = LM3643_TORCH_BR_CDEV_MAX;
+	led_cdev->brightness_set_blocking = lm3643_torch_set_brightness;
+	led_cdev->brightness_get = lm3643_torch_get_brightness;
+
+	ret = lm3643_apply_fw_limits(dev, led, fwnode);
+	if (ret)
+		return ret;
+
+	torch_max_brightness = led_cdev->max_brightness;
+
+	flash_cdev->brightness.max = min(flash_cdev->brightness.max, LM3643_FLASH_BR_UA_TOTAL_MAX);
+	flash_cdev->brightness.val = min(flash_cdev->brightness.val, flash_cdev->brightness.max);
+	flash_cdev->timeout.val = min(flash_cdev->timeout.val, flash_cdev->timeout.max);
+
+	ret = lm3643_flash_brightness_set(flash_cdev, flash_cdev->brightness.val);
+	if (ret)
+		return ret;
+
+	ret = devm_led_classdev_flash_register_ext(dev, flash_cdev, &init_data);
+	if (ret)
+		return ret;
+
+	if (led_cdev->max_brightness > torch_max_brightness) {
+		dev_warn(dev, "max-brightness %u not supported (using %u)\n",
+			 led_cdev->max_brightness,
+			 torch_max_brightness);
+		led_cdev->max_brightness = torch_max_brightness;
+	}
+
+	return lm3643_register_v4l2(dev, led, fwnode);
+}
+
+static int lm3643_probe(struct i2c_client *client)
+{
+	struct device *dev = &client->dev;
+	struct lm3643 *chip;
+	unsigned int dev_id;
+	unsigned int count;
+	int ret;
+
+	count = device_get_child_node_count(dev);
+	if (!count || count > LM3643_NUM_CHANNELS)
+		return dev_err_probe(dev, -EINVAL, "%u LED nodes found, expected 1 to %d\n",
+					 count, LM3643_NUM_CHANNELS);
+
+	chip = devm_kzalloc(dev, sizeof(*chip), GFP_KERNEL);
+	if (!chip)
+		return -ENOMEM;
+	i2c_set_clientdata(client, chip);
+
+	chip->regmap = devm_regmap_init_i2c(client, &lm3643_regmap_config);
+	if (IS_ERR(chip->regmap))
+		return dev_err_probe(dev,
+				     PTR_ERR(chip->regmap),
+				     "failed to allocate register map\n");
+
+	ret = regmap_read(chip->regmap, LM3643_REG_DEV_ID, &dev_id);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to read device ID\n");
+
+	dev_id &= LM3643_DEV_ID_MASK;
+	if (dev_id != LM3643_DEV_ID)
+		return dev_err_probe(dev, -ENODEV, "wrong chip id 0x%02x (expected 0x%02x)\n",
+					 dev_id, LM3643_DEV_ID);
+
+	ret = devm_mutex_init(dev, &chip->lock);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to create mutex\n");
+
+	ret = regmap_write(chip->regmap, LM3643_REG_ENABLE, LM3643_MODE_STANDBY);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to put device in standby mode\n");
+	ret = devm_add_action_or_reset(dev, lm3643_standby, chip->regmap);
+	if (ret)
+		return ret;
+
+	ret = regmap_update_bits(chip->regmap, LM3643_REG_TORCH_BR_LED1,
+				 LM3643_TORCH_BR_LED2_OVERRIDE,
+				 0);
+	if (ret)
+		return dev_err_probe(dev,
+				     ret,
+				     "failed to clear LED2 torch current override register\n");
+
+	ret = regmap_update_bits(chip->regmap, LM3643_REG_FLASH_BR_LED1,
+				 LM3643_FLASH_BR_LED2_OVERRIDE,
+				 0);
+	if (ret)
+		return dev_err_probe(dev,
+				     ret,
+				     "failed to clear LED2 flash current override register\n");
+
+	device_for_each_child_node_scoped(dev, child) {
+		ret = lm3643_register_led(dev, chip, child);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
+static const struct acpi_device_id lm3643_acpi_leds_match[] = {
+	{ "TXNW3643" },
+	{ },
+};
+MODULE_DEVICE_TABLE(acpi, lm3643_acpi_leds_match);
+
+static const struct of_device_id lm3643_of_leds_match[] = {
+	{ .compatible = "ti,lm3643" },
+	{ },
+};
+MODULE_DEVICE_TABLE(of, lm3643_of_leds_match);
+
+static struct i2c_driver lm3643_i2c_driver = {
+	.driver = {
+		.name = "lm3643",
+		.acpi_match_table = lm3643_acpi_leds_match,
+		.of_match_table = lm3643_of_leds_match,
+	},
+	.probe = lm3643_probe,
+};
+module_i2c_driver(lm3643_i2c_driver);
+
+MODULE_DESCRIPTION("Texas Instruments LM3643 LED Flash Driver");
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Rillian Grant <rillian.grant@gmail.com>");
-- 
2.55.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH 0/2] leds: flash: lm3643: standby fix and ACPI LED description
       [not found] <20260821083620.68324-1-rillian.grant@gmail.com>
  2026-08-21  8:36 ` [RFC PATCH 1/2] dt-bindings: leds: Document TI LM3643 dual LED flash driver Rillian Grant
  2026-08-21  8:36 ` [RFC PATCH 2/2] leds: flash: Add support for the " Rillian Grant
@ 2026-09-24 17:20 ` Ruslan Koreev
  2026-09-24 17:20   ` [PATCH 1/2] leds: flash: lm3643: Return to standby when the last output is switched off Ruslan Koreev
  2026-09-24 17:20   ` [PATCH 2/2] leds: flash: lm3643: Describe the LED of ACPI boards without LED nodes Ruslan Koreev
  2 siblings, 2 replies; 5+ messages in thread
From: Ruslan Koreev @ 2026-09-24 17:20 UTC (permalink / raw)
  To: lee, pavel, rillian.grant; +Cc: linux-leds, linux-kernel, Ruslan Koreev

These apply on top of Rillian Grant's "[RFC PATCH 0/2] leds: flash:
LM3643 dual LED flash driver" and answer two things from that thread.

Patch 1 addresses the two state-machine problems flagged by the Sashiko
review of the RFC, which Lee Jones asked to have addressed: switching
the last active output off left the chip in torch or flash mode instead
of standby.

Patch 2 addresses the question in the RFC cover letter about supplying
a default configuration on ACPI machines, where the chip is enumerated
as TXNW3643 without LED child nodes: it adds a DMI table describing the
LED as a software node, starting with the Lenovo ThinkPad X1 Carbon
Gen 14, where the LM3643 drives the Windows Hello IR flood illuminator
with both current sources feeding one LED. The limits are 100 mA torch
and 300 mA flash per output, well below the chip's maximums.

Tested on that laptop with Fedora 44 (kernel 7.2.5): the LED appears as
ir:flash, torch mode lights the ST VD55G1 IR camera's field of view and
the chip returns to standby when the torch is switched off, and face
authentication with howdy works on those frames.

Rillian, if you prefer to fold these into a v2 of your series, please
do; a DMI entry for the X9-15 Gen 1 only needs its product version
string.

Ruslan Koreev (2):
  leds: flash: lm3643: Return to standby when the last output is
    switched off
  leds: flash: lm3643: Describe the LED of ACPI boards without LED nodes

 drivers/leds/flash/leds-lm3643.c | 115 +++++++++++++++++++++++++++++--
 1 file changed, 109 insertions(+), 6 deletions(-)

-- 
2.55.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH 1/2] leds: flash: lm3643: Return to standby when the last output is switched off
  2026-09-24 17:20 ` [PATCH 0/2] leds: flash: lm3643: standby fix and ACPI LED description Ruslan Koreev
@ 2026-09-24 17:20   ` Ruslan Koreev
  2026-09-24 17:20   ` [PATCH 2/2] leds: flash: lm3643: Describe the LED of ACPI boards without LED nodes Ruslan Koreev
  1 sibling, 0 replies; 5+ messages in thread
From: Ruslan Koreev @ 2026-09-24 17:20 UTC (permalink / raw)
  To: lee, pavel, rillian.grant; +Cc: linux-leds, linux-kernel, Ruslan Koreev

Setting the torch brightness of an output to 0 only cleared its enable
bit and left the mode bits at torch (or flash), so the chip kept its
boost converter running with no output enabled. Likewise switching the
strobe off returned early when the output's enable bit had already been
cleared, leaving the mode bits at flash. Both were flagged by the
Sashiko review of the RFC, which Lee Jones asked to have addressed.

Clear the mode bits together with the enable bit whenever the sibling
output is not active, so that the chip goes back to standby when its
last output is switched off.

Signed-off-by: Ruslan Koreev <koreev.r@gmail.com>
---
 drivers/leds/flash/leds-lm3643.c | 23 +++++++++++++++++------
 1 file changed, 17 insertions(+), 6 deletions(-)

diff --git a/drivers/leds/flash/leds-lm3643.c b/drivers/leds/flash/leds-lm3643.c
index 48e164261..bbe9358c9 100644
--- a/drivers/leds/flash/leds-lm3643.c
+++ b/drivers/leds/flash/leds-lm3643.c
@@ -271,9 +271,15 @@ static int lm3643_torch_set_brightness(struct led_classdev *led_cdev,
 	    mode != LM3643_MODE_TORCH)
 		return -EBUSY;
 
-	if (brightness == 0)
-		return regmap_update_bits(led->regmap, LM3643_REG_ENABLE,
-				chan->enable_bit, 0);
+	if (brightness == 0) {
+		unsigned int mask = chan->enable_bit;
+
+		/* Last active output: leave torch mode, back to standby */
+		if (!(enable & sibling_bit))
+			mask |= LM3643_MODE_MASK;
+
+		return regmap_update_bits(led->regmap, LM3643_REG_ENABLE, mask, 0);
+	}
 
 	ret = regmap_update_bits(led->regmap, chan->torch_br_reg, LM3643_TORCH_BR_MASK,
 				 LM3643_TORCH_BR_CDEV_TO_CODE(brightness));
@@ -365,11 +371,16 @@ static int lm3643_flash_strobe_set(struct led_classdev_flash *fled_cdev, bool st
 	mode = enable_reg & LM3643_MODE_MASK;
 
 	if (!state) {
-		if (mode != LM3643_MODE_FLASH || !(enable_reg & chan->enable_bit))
+		unsigned int mask = chan->enable_bit;
+
+		if (mode != LM3643_MODE_FLASH)
 			return 0;
 
-		return regmap_update_bits(led->regmap, LM3643_REG_ENABLE,
-			LM3643_MODE_MASK, LM3643_MODE_STANDBY);
+		/* Leave flash mode once no other output is strobing */
+		if (!(enable_reg & sibling_bit))
+			mask |= LM3643_MODE_MASK;
+
+		return regmap_update_bits(led->regmap, LM3643_REG_ENABLE, mask, 0);
 	}
 	if ((enable_reg & sibling_bit) && mode != LM3643_MODE_STANDBY)
 		return -EBUSY;
-- 
2.55.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH 2/2] leds: flash: lm3643: Describe the LED of ACPI boards without LED nodes
  2026-09-24 17:20 ` [PATCH 0/2] leds: flash: lm3643: standby fix and ACPI LED description Ruslan Koreev
  2026-09-24 17:20   ` [PATCH 1/2] leds: flash: lm3643: Return to standby when the last output is switched off Ruslan Koreev
@ 2026-09-24 17:20   ` Ruslan Koreev
  1 sibling, 0 replies; 5+ messages in thread
From: Ruslan Koreev @ 2026-09-24 17:20 UTC (permalink / raw)
  To: lee, pavel, rillian.grant; +Cc: linux-leds, linux-kernel, Ruslan Koreev

Lenovo ThinkPads enumerate the LM3643 through ACPI (HID TXNW3643)
without a _DSD, so the device has no LED child nodes and the driver
refuses to probe. On these laptops the chip drives the Windows Hello IR
flood illuminator, with both current sources feeding a single LED, and
Lenovo's Windows driver applies a fixed configuration to any LM3643 it
finds.

Add a DMI table that provides that description as a software node when
the firmware offers none: one IR flash LED on both outputs, with torch
and flash limits well below the chip's maximums. Start with the ThinkPad
X1 Carbon Gen 14.

The description is keyed by DMI instead of being applied to every
TXNW3643 because the wiring is a board property: which output feeds
which LED and how much current the LED tolerates cannot be read from the
chip, and a wrong guess overdrives an LED. The firmware cannot be fixed
from the driver side, and a per-board table is what x86-android-tablets
does for devices whose ACPI tables describe nothing either. Boards with
a _DSD are not affected, the fallback only runs when the device has no
child nodes. The limits are conservative because the LED part is not
documented; the illuminator is bright enough for face authentication at
a fraction of them.

Tested on that laptop: torch through /sys/class/leds/ir:flash lights the
ST VD55G1 IR camera's field of view, and the resulting frames are good
enough for face authentication.

Signed-off-by: Ruslan Koreev <koreev.r@gmail.com>
---
 drivers/leds/flash/leds-lm3643.c | 92 ++++++++++++++++++++++++++++++++
 1 file changed, 92 insertions(+)

diff --git a/drivers/leds/flash/leds-lm3643.c b/drivers/leds/flash/leds-lm3643.c
index bbe9358c9..fa0ecead5 100644
--- a/drivers/leds/flash/leds-lm3643.c
+++ b/drivers/leds/flash/leds-lm3643.c
@@ -9,6 +9,7 @@
 #include <linux/bitfield.h>
 #include <linux/bits.h>
 #include <linux/cleanup.h>
+#include <linux/dmi.h>
 #include <linux/i2c.h>
 #include <linux/led-class-flash.h>
 #include <linux/leds.h>
@@ -690,6 +691,91 @@ static int lm3643_register_led(struct device *dev, struct lm3643 *chip,
 	return lm3643_register_v4l2(dev, led, fwnode);
 }
 
+/*
+ * Lenovo ThinkPads enumerate the chip through ACPI (HID TXNW3643) without a
+ * _DSD, so the LED is not described by firmware. On the boards below it drives
+ * the Windows Hello IR flood illuminator, with both current sources feeding a
+ * single LED. Describe that with a software node, with torch and flash limits
+ * (100 mA and 300 mA per output) well below the chip's maximums.
+ */
+static const u32 lm3643_ir_flood_sources[] = { 0, 1 };
+
+static const struct property_entry lm3643_ir_flood_props[] = {
+	PROPERTY_ENTRY_U32_ARRAY("led-sources", lm3643_ir_flood_sources),
+	PROPERTY_ENTRY_STRING("function", LED_FUNCTION_FLASH),
+	PROPERTY_ENTRY_U32("color", LED_COLOR_ID_IR),
+	PROPERTY_ENTRY_U32("led-max-microamp", 200000),
+	PROPERTY_ENTRY_U32("flash-max-microamp", 600000),
+	PROPERTY_ENTRY_U32("flash-max-timeout-us", 100000),
+	{ }
+};
+
+static const struct software_node lm3643_ir_flood_root = {
+	.name = "lm3643",
+};
+
+static const struct software_node lm3643_ir_flood_led = {
+	.name = "led-0",
+	.parent = &lm3643_ir_flood_root,
+	.properties = lm3643_ir_flood_props,
+};
+
+static const struct software_node *lm3643_ir_flood_nodes[] = {
+	&lm3643_ir_flood_root,
+	&lm3643_ir_flood_led,
+	NULL
+};
+
+static const struct dmi_system_id lm3643_dmi_leds[] = {
+	{
+		/* Lenovo ThinkPad X1 Carbon Gen 14 */
+		.matches = {
+			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
+			DMI_MATCH(DMI_PRODUCT_VERSION, "ThinkPad X1 Carbon Gen 14"),
+		},
+		.driver_data = (void *)lm3643_ir_flood_nodes,
+	},
+	{ }
+};
+
+static void lm3643_remove_dmi_leds(void *data)
+{
+	struct device *dev = data;
+	const struct dmi_system_id *id = dmi_first_match(lm3643_dmi_leds);
+
+	set_secondary_fwnode(dev, NULL);
+	if (id)
+		software_node_unregister_node_group(id->driver_data);
+}
+
+static int lm3643_add_dmi_leds(struct device *dev)
+{
+	const struct dmi_system_id *id;
+	const struct software_node **nodes;
+	int ret;
+
+	if (!has_acpi_companion(dev))
+		return 0;
+
+	id = dmi_first_match(lm3643_dmi_leds);
+	if (!id)
+		return 0;
+
+	nodes = id->driver_data;
+	/* The nodes are static: a second chip on the same board cannot reuse them */
+	if (software_node_fwnode(nodes[0]))
+		return dev_err_probe(dev, -EBUSY, "LED description already in use\n");
+
+	ret = software_node_register_node_group(nodes);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to register the LED description\n");
+
+	set_secondary_fwnode(dev, software_node_fwnode(nodes[0]));
+	dev_info(dev, "no LED nodes in firmware, using the DMI description\n");
+
+	return devm_add_action_or_reset(dev, lm3643_remove_dmi_leds, dev);
+}
+
 static int lm3643_probe(struct i2c_client *client)
 {
 	struct device *dev = &client->dev;
@@ -699,6 +785,12 @@ static int lm3643_probe(struct i2c_client *client)
 	int ret;
 
 	count = device_get_child_node_count(dev);
+	if (!count) {
+		ret = lm3643_add_dmi_leds(dev);
+		if (ret)
+			return ret;
+		count = device_get_child_node_count(dev);
+	}
 	if (!count || count > LM3643_NUM_CHANNELS)
 		return dev_err_probe(dev, -EINVAL, "%u LED nodes found, expected 1 to %d\n",
 					 count, LM3643_NUM_CHANNELS);
-- 
2.55.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2026-09-24 17:21 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
     [not found] <20260821083620.68324-1-rillian.grant@gmail.com>
2026-08-21  8:36 ` [RFC PATCH 1/2] dt-bindings: leds: Document TI LM3643 dual LED flash driver Rillian Grant
2026-08-21  8:36 ` [RFC PATCH 2/2] leds: flash: Add support for the " Rillian Grant
2026-09-24 17:20 ` [PATCH 0/2] leds: flash: lm3643: standby fix and ACPI LED description Ruslan Koreev
2026-09-24 17:20   ` [PATCH 1/2] leds: flash: lm3643: Return to standby when the last output is switched off Ruslan Koreev
2026-09-24 17:20   ` [PATCH 2/2] leds: flash: lm3643: Describe the LED of ACPI boards without LED nodes Ruslan Koreev

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®