mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH v2 0/4] leds: flash: LM3643 dual LED flash driver
@ 2026-09-28  9:42 Rillian Grant
  2026-09-28  9:42 ` [PATCH v2 1/4] dt-bindings: leds: Document TI " Rillian Grant
                   ` (3 more replies)
  0 siblings, 4 replies; 5+ messages in thread
From: Rillian Grant @ 2026-09-28  9:42 UTC (permalink / raw)
  To: Lee Jones, Pavel Machek
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Ruslan Koreev,
	linux-leds, devicetree, linux-kernel, Rillian Grant

This series adds a flash LED driver for the Texas Instruments
LM3643. I wrote it to support the Windows Hello NIR illuminator on my
ThinkPad X9-15 Gen 1, which has a single LED connected to both current
sources. The driver has also been tested by Ruslan Koreev on a ThinkPad
X1 Carbon Gen 14.

On both ThinkPad models the chip is described by ACPI but has no _DSD.
The original RFC asked how to supply the missing child nodes. This
series includes DMI-matched software nodes for both laptop models.

1/4 DT binding
2/4 driver
3/4 DMI match table and values for the X1 Carbon
4/4 DMI entry for the X9-15 Gen 1

Changes in v2:
- Rebased onto leds/for-leds-next
- dt-bindings: make flash-max-microamp, led-max-microamp and
  flash-max-timeout-us required
- driver: return device to standby state when the last output is
  switched off (squashed fix provided by Ruslan Koreev)
- New 3/4: DMI mechanism and entry for the X1 Carbon developed by
  Ruslan. Plus a minor update to provide a reg property for consistency
  with the DT bindings.
- New 4/4: DMI entry for the X9-15 Gen 1 and update struct names to be
  model-specific.

v1: https://lore.kernel.org/all/20260821083620.68324-1-rillian.grant@gmail.com/

Rillian Grant (3):
  dt-bindings: leds: Document TI LM3643 dual LED flash driver
  leds: flash: Add support for the TI LM3643 dual LED flash driver
  leds: flash: lm3643: Describe the LED on the Lenovo ThinkPad X9-15 Gen
    1

Ruslan Koreev (1):
  leds: flash: lm3643: Describe the LED of ACPI boards without LED nodes

 .../devicetree/bindings/leds/ti,lm3643.yaml   | 129 +++
 MAINTAINERS                                   |   7 +
 drivers/leds/flash/Kconfig                    |  12 +
 drivers/leds/flash/Makefile                   |   1 +
 drivers/leds/flash/leds-lm3643.c              | 919 ++++++++++++++++++
 5 files changed, 1068 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/leds/ti,lm3643.yaml
 create mode 100644 drivers/leds/flash/leds-lm3643.c


base-commit: 05b4738b0078f7d6f154f68068a11c8a0635e9df
-- 
2.55.0


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

* [PATCH v2 1/4] dt-bindings: leds: Document TI LM3643 dual LED flash driver
  2026-09-28  9:42 [PATCH v2 0/4] leds: flash: LM3643 dual LED flash driver Rillian Grant
@ 2026-09-28  9:42 ` Rillian Grant
  2026-09-28  9:42 ` [PATCH v2 2/4] leds: flash: Add support for the " Rillian Grant
                   ` (2 subsequent siblings)
  3 siblings, 0 replies; 5+ messages in thread
From: Rillian Grant @ 2026-09-28  9:42 UTC (permalink / raw)
  To: Lee Jones, Pavel Machek
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Ruslan Koreev,
	linux-leds, devicetree, linux-kernel, Rillian Grant

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   | 129 ++++++++++++++++++
 1 file changed, 129 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..c212f2345d92
--- /dev/null
+++ b/Documentation/devicetree/bindings/leds/ti,lm3643.yaml
@@ -0,0 +1,129 @@
+# 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
+      - flash-max-microamp
+      - led-max-microamp
+      - flash-max-timeout-us
+
+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

* [PATCH v2 2/4] leds: flash: Add support for the TI LM3643 dual LED flash driver
  2026-09-28  9:42 [PATCH v2 0/4] leds: flash: LM3643 dual LED flash driver Rillian Grant
  2026-09-28  9:42 ` [PATCH v2 1/4] dt-bindings: leds: Document TI " Rillian Grant
@ 2026-09-28  9:42 ` Rillian Grant
  2026-09-28  9:42 ` [PATCH v2 3/4] leds: flash: lm3643: Describe the LED of ACPI boards without LED nodes Rillian Grant
  2026-09-28  9:42 ` [PATCH v2 4/4] leds: flash: lm3643: Describe the LED on the Lenovo ThinkPad X9-15 Gen 1 Rillian Grant
  3 siblings, 0 replies; 5+ messages in thread
From: Rillian Grant @ 2026-09-28  9:42 UTC (permalink / raw)
  To: Lee Jones, Pavel Machek
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Ruslan Koreev,
	linux-leds, devicetree, linux-kernel, Rillian Grant

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).

Co-developed-by: Ruslan Koreev <koreev.r@gmail.com>
Signed-off-by: Ruslan Koreev <koreev.r@gmail.com>
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 | 786 +++++++++++++++++++++++++++++++
 4 files changed, 806 insertions(+)
 create mode 100644 drivers/leds/flash/leds-lm3643.c

diff --git a/MAINTAINERS b/MAINTAINERS
index a1eb0937238d..0c186a7f2648 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -27036,6 +27036,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..bbe9358c9caa
--- /dev/null
+++ b/drivers/leds/flash/leds-lm3643.c
@@ -0,0 +1,786 @@
+// 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) {
+		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));
+	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) {
+		unsigned int mask = chan->enable_bit;
+
+		if (mode != LM3643_MODE_FLASH)
+			return 0;
+
+		/* 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;
+
+	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 v2 3/4] leds: flash: lm3643: Describe the LED of ACPI boards without LED nodes
  2026-09-28  9:42 [PATCH v2 0/4] leds: flash: LM3643 dual LED flash driver Rillian Grant
  2026-09-28  9:42 ` [PATCH v2 1/4] dt-bindings: leds: Document TI " Rillian Grant
  2026-09-28  9:42 ` [PATCH v2 2/4] leds: flash: Add support for the " Rillian Grant
@ 2026-09-28  9:42 ` Rillian Grant
  2026-09-28  9:42 ` [PATCH v2 4/4] leds: flash: lm3643: Describe the LED on the Lenovo ThinkPad X9-15 Gen 1 Rillian Grant
  3 siblings, 0 replies; 5+ messages in thread
From: Rillian Grant @ 2026-09-28  9:42 UTC (permalink / raw)
  To: Lee Jones, Pavel Machek
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Ruslan Koreev,
	linux-leds, devicetree, linux-kernel, Rillian Grant

From: Ruslan Koreev <koreev.r@gmail.com>

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>
[rillian: added reg property to match the DT binding]
Signed-off-by: Rillian Grant <rillian.grant@gmail.com>
---
 drivers/leds/flash/leds-lm3643.c | 93 ++++++++++++++++++++++++++++++++
 1 file changed, 93 insertions(+)

diff --git a/drivers/leds/flash/leds-lm3643.c b/drivers/leds/flash/leds-lm3643.c
index bbe9358c9caa..1c0de38ebcdf 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,92 @@ 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("reg", 0),
+	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 +786,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

* [PATCH v2 4/4] leds: flash: lm3643: Describe the LED on the Lenovo ThinkPad X9-15 Gen 1
  2026-09-28  9:42 [PATCH v2 0/4] leds: flash: LM3643 dual LED flash driver Rillian Grant
                   ` (2 preceding siblings ...)
  2026-09-28  9:42 ` [PATCH v2 3/4] leds: flash: lm3643: Describe the LED of ACPI boards without LED nodes Rillian Grant
@ 2026-09-28  9:42 ` Rillian Grant
  3 siblings, 0 replies; 5+ messages in thread
From: Rillian Grant @ 2026-09-28  9:42 UTC (permalink / raw)
  To: Lee Jones, Pavel Machek
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Ruslan Koreev,
	linux-leds, devicetree, linux-kernel, Rillian Grant

The ThinkPad X9-15 Gen 1 enumerates the LM3643 through ACPI (HID
TXNW3643) without a _DSD, like the X1 Carbon Gen 14. It drives the
Windows Hello IR flood illuminator with both current sources feeding a
single LED.

The flash limits were obtained from an ETW trace of the SPB class
extension during a Windows Hello unlock. The Windows driver programs
304 mA per current source (code 0x19 with the LED2 override set) and a
400 ms flash timeout.

There is no vendor value for the torch limit, so use the same
conservative limit as the X1 Carbon Gen 14.

Rename the X1 Carbon Gen 14 nodes after the machine now that the table
has a second entry. Sources and root node remain shared.

Signed-off-by: Rillian Grant <rillian.grant@gmail.com>
---
 drivers/leds/flash/leds-lm3643.c | 70 +++++++++++++++++++++++++-------
 1 file changed, 55 insertions(+), 15 deletions(-)

diff --git a/drivers/leds/flash/leds-lm3643.c b/drivers/leds/flash/leds-lm3643.c
index 1c0de38ebcdf..b9d167e25375 100644
--- a/drivers/leds/flash/leds-lm3643.c
+++ b/drivers/leds/flash/leds-lm3643.c
@@ -691,16 +691,21 @@ 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[] = {
+static const struct software_node lm3643_ir_flood_root = {
+	.name = "lm3643",
+};
+
+/*
+ * The Lenovo ThinkPad X1 Carbon Gen 14 enumerates the chip through ACPI
+ * (HID TXNW3643) without a _DSD, so the LED is not described by firmware.
+ * On the X1 Gen 14 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 struct property_entry lm3643_x1c14_props[] = {
 	PROPERTY_ENTRY_U32("reg", 0),
 	PROPERTY_ENTRY_U32_ARRAY("led-sources", lm3643_ir_flood_sources),
 	PROPERTY_ENTRY_STRING("function", LED_FUNCTION_FLASH),
@@ -711,19 +716,46 @@ static const struct property_entry lm3643_ir_flood_props[] = {
 	{ }
 };
 
-static const struct software_node lm3643_ir_flood_root = {
-	.name = "lm3643",
+static const struct software_node lm3643_x1c14_led = {
+	.name = "led-0",
+	.parent = &lm3643_ir_flood_root,
+	.properties = lm3643_x1c14_props,
+};
+
+static const struct software_node *lm3643_x1c14_nodes[] = {
+	&lm3643_ir_flood_root,
+	&lm3643_x1c14_led,
+	NULL
 };
 
-static const struct software_node lm3643_ir_flood_led = {
+/*
+ * The Lenovo ThinkPad X9-15 Gen 1 enumerates the chip through ACPI
+ * (HID TXNW3643) without a _DSD. On the X9-15 it drives the Windows Hello IR
+ * flood illuminator, with both current sources feeding a single LED. Flash
+ * maximums were obtained from an SPB class extension ETW trace of the
+ * vendor-supplied Windows driver. The torch limit is a conservative value well
+ * below the chip's maximum.
+ */
+static const struct property_entry lm3643_x9_15_props[] = {
+	PROPERTY_ENTRY_U32("reg", 0),
+	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", 608050),
+	PROPERTY_ENTRY_U32("flash-max-timeout-us", 400000),
+	{ }
+};
+
+static const struct software_node lm3643_x9_15_led = {
 	.name = "led-0",
 	.parent = &lm3643_ir_flood_root,
-	.properties = lm3643_ir_flood_props,
+	.properties = lm3643_x9_15_props,
 };
 
-static const struct software_node *lm3643_ir_flood_nodes[] = {
+static const struct software_node *lm3643_x9_15_nodes[] = {
 	&lm3643_ir_flood_root,
-	&lm3643_ir_flood_led,
+	&lm3643_x9_15_led,
 	NULL
 };
 
@@ -734,7 +766,15 @@ static const struct dmi_system_id lm3643_dmi_leds[] = {
 			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
 			DMI_MATCH(DMI_PRODUCT_VERSION, "ThinkPad X1 Carbon Gen 14"),
 		},
-		.driver_data = (void *)lm3643_ir_flood_nodes,
+		.driver_data = (void *)lm3643_x1c14_nodes,
+	},
+	{
+		/* Lenovo ThinkPad X9-15 Gen 1 */
+		.matches = {
+			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
+			DMI_MATCH(DMI_PRODUCT_VERSION, "ThinkPad X9-15 Gen 1"),
+		},
+		.driver_data = (void *)lm3643_x9_15_nodes,
 	},
 	{ }
 };
-- 
2.55.0


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

end of thread, other threads:[~2026-09-28  9:43 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-28  9:42 [PATCH v2 0/4] leds: flash: LM3643 dual LED flash driver Rillian Grant
2026-09-28  9:42 ` [PATCH v2 1/4] dt-bindings: leds: Document TI " Rillian Grant
2026-09-28  9:42 ` [PATCH v2 2/4] leds: flash: Add support for the " Rillian Grant
2026-09-28  9:42 ` [PATCH v2 3/4] leds: flash: lm3643: Describe the LED of ACPI boards without LED nodes Rillian Grant
2026-09-28  9:42 ` [PATCH v2 4/4] leds: flash: lm3643: Describe the LED on the Lenovo ThinkPad X9-15 Gen 1 Rillian Grant

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®