All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v8 0/2] Subject: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver
@ 2026-07-28 11:09 mohit.dsor
  2026-07-28 11:09 ` [PATCH v8 1/2] dt-bindings: bridge: " mohit.dsor
  2026-07-28 11:09 ` [PATCH v8 2/2] drm/bridge: " mohit.dsor
  0 siblings, 2 replies; 5+ messages in thread
From: mohit.dsor @ 2026-07-28 11:09 UTC (permalink / raw)
  To: Sunyun Yang, Andrzej Hajda, Neil Armstrong, Robert Foss,
	Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
	Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Vinod Koul
  Cc: dri-devel, devicetree, linux-kernel, Mohit Dsor, venkata.valluru,
	Jessica Zhang, Mohit Dsor

This series introduces:
  - A device tree binding YAML file describing the hardware
  - A new DRM bridge driver implementing the basic functionality

  LT9611C(EX/UXD) is an I2C-controlled chip that Receiver signal/dual port
  mipi dsi and output hdmi, differences in hardware features:
  - LT9611C: supports 1-port mipi dsi to hdmi 1.4
  - LT9611EX: supports 2-port mipi dsi to hdmi 1.4
  - LT9611UXD: supports 2-port mipi dsi to hdmi 1.4/2.0

  Changes in v8:
  - Refactor lt9611c_read_write_flow() to take a struct lt9611c_cmd parameter with an embedded struct lt9611c_cmd_hdr (func,
  type, seq, sep) and optional data payload, eliminating raw byte arrays at call sites
  - Add LT9611C_CMD_HDR_SIZE, LT9611C_EDID_BUF_SIZE constants to replace magic numbers
  - Use container_of_const() in bridge_to_lt9611c_const()
  - Remove obvious /*read only*/ comment
  - Use unsigned long cast instead of uintptr_t for chip type initialization
  - Replace memset + memcpy with memcpy + memset for CRC buffer preparation
  - Add ABI documentation for lt9611c_firmware sysfs attribute
  - Fix Kconfig help text to proper prose within line limits
  - Add disable_irq() before cancel_work_sync() in lt9611c_remove()
  - Add cancel_work_sync() in lt9611c_bridge_suspend()
  - Inline lt9611c_gpio_init() into probe
  - Use of_drm_get_bridge_by_endpoint() instead of drm_of_find_panel_or_bridge() + drm_bridge_get()
  - Return error if LT9611C (single DSI only) is configured with dual DSI in device tree
  - Move HPD status read_write_flow call from IRQ handler to lt9611c_hpd_work workqueue
  - Removed second lt9611_reset call from probe as not needed
  - Add struct lt9611c_rsp symmetric to struct lt9611c_cmd — both embed struct lt9611c_cmd_hdr
  - Link to v7: https://lore.kernel.org/lkml/20260716-lt9611c-v7-v7-0-7553a14735fc@oss.qualcomm.com/

  Changes in v7:
  - Use i2c_get_match_data() for chip type initialization
  - Implement hdmi_write_hdmi_infoframe/hdmi_clear_hdmi_infoframe
  - Use drm_atomic_commit in bridge callbacks
  - Removing redundant lt9611c_reset() call and cleanup in probe error
    path
  - Populate bridge.vendor and bridge.product
  - implement hpd_enable callback
  - fix DT parsing and bridge refcount
  - Removed pre_enable and post_disable callbacks, it was affecting hpd
    as regulator was disabled
  - Changed reset gpio enablement and lt9611c_reset api.
  - Link to v6: https://lore.kernel.org/all/20260508134009.4582-1-syyang@lontium.com/

  Changes in v6:
  - dt-binding:
   1. sorted the compatible list alphabetically.                     [Dmitry]
  - Link to v5: https://lore.kernel.org/lkml/20260507024214.97708-1-syyang@lontium.com/

  Changes in v5:
  - drm/bridge:
   1. use #define FW_FILE "Lontium/lt9611c_fw.bin" to match linux-firmware    [Dmitry]
   2. add atomic state management
  - Link to v4: https://lore.kernel.org/lkml/20260506095100.78998-1-syyang@lontium.com/

  Changes in v4:
  - dt-binding:
   1. fix commit message                                                    [Krzysztof]
  - drm/bridge:
   1. use lt9611c_fw.bin                                                     [Dmitry]
   3. use enum lt9611_chip_type
   4. remove *fw from the lt9611c struct
   5. use read_poll_timeout and -ETIMEDOUT
   6. replace mutex_lock with guard(mutex) in some places
   7. switch to ARRAY_SIZE instead of specifying lengths directly
   8. use dev_err_probe
   9. add clear functions for AVI and audio infoframes
   10. add DRM_BRIDGE_OP_HDMI and remove .hpd_notify
   11. replace "GPL v2" with "GPL"
  - Link to v3: https://lore.kernel.org/lkml/20260420061644.1251070-1-syyang@lontium.com/

  Changes in v3:
  - dt-binding:
   1. lt9611c(ex/uxd) content merged into lontium,lt9611.yaml
  - drm/bridge:
   1. Drop the licence text, only use SPDX header
   2. Sort the headers
   3. Use library functions for crc8
   4. Use paged writes as implemented for LT9611C(EX/UXD)
   5. Use HDMI audio helpers
   6. Replace devm_kzalloc with devm_drm_bridge_alloc
   7. Remove extra kthread
  - Link to v1: https://lore.kernel.org/lkml/20250903123825.1721443-1-syyang@lontium.com/

  Changes in v2:
   1. Forget modify code, operation error, Please disregard this submit.

  Changes in v1:
  - dt-binding:
   1. Submit the first version of the code.
  - drm/bridge:
   1. Submit the first version of the code.

Signed-off-by: Mohit Dsor <mohit.dsor@oss.qualcomm.com>
---
Sunyun Yang (2):
      dt-bindings: bridge: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver
      drm/bridge: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver

 .../ABI/testing/sysfs-driver-lontium-lt9611c       |   11 +
 .../bindings/display/bridge/lontium,lt9611.yaml    |    8 +-
 drivers/gpu/drm/bridge/Kconfig                     |   17 +
 drivers/gpu/drm/bridge/Makefile                    |    1 +
 drivers/gpu/drm/bridge/lontium-lt9611c.c           | 1344 ++++++++++++++++++++
 5 files changed, 1379 insertions(+), 2 deletions(-)
---
base-commit: 34e27b90552acf21bd73aac395667c8aae6da480
change-id: 20260708-lt9611c-v7-ff76b2d1bf5f

Best regards,
--  
Mohit Dsor <mohit.dsor@oss.qualcomm.com>


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

* [PATCH v8 1/2] dt-bindings: bridge: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver
  2026-07-28 11:09 [PATCH v8 0/2] Subject: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver mohit.dsor
@ 2026-07-28 11:09 ` mohit.dsor
  2026-07-28 11:14   ` sashiko-bot
  2026-07-28 11:09 ` [PATCH v8 2/2] drm/bridge: " mohit.dsor
  1 sibling, 1 reply; 5+ messages in thread
From: mohit.dsor @ 2026-07-28 11:09 UTC (permalink / raw)
  To: Sunyun Yang, Andrzej Hajda, Neil Armstrong, Robert Foss,
	Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
	Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Vinod Koul
  Cc: dri-devel, devicetree, linux-kernel, Mohit Dsor, venkata.valluru,
	Jessica Zhang, Mohit Dsor

From: Sunyun Yang <syyang@lontium.com>

LT9611C(EX/UXD) is an I2C-controlled chip that Receiver signal/dual port
mipi dsi and output hdmi, differences in hardware features:
- LT9611C: supports 1-port mipi dsi to hdmi 1.4
- LT9611EX: supports 2-port mipi dsi to hdmi 1.4
- LT9611UXD: supports 2-port mipi dsi to hdmi 1.4/2.0

Signed-off-by: Sunyun Yang <syyang@lontium.com>
Signed-off-by: Mohit Dsor <mdsor@oss.qualcomm.com>
---
 .../devicetree/bindings/display/bridge/lontium,lt9611.yaml        | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/Documentation/devicetree/bindings/display/bridge/lontium,lt9611.yaml b/Documentation/devicetree/bindings/display/bridge/lontium,lt9611.yaml
index 429a06057ae8..e0821a63d9d7 100644
--- a/Documentation/devicetree/bindings/display/bridge/lontium,lt9611.yaml
+++ b/Documentation/devicetree/bindings/display/bridge/lontium,lt9611.yaml
@@ -4,19 +4,23 @@
 $id: http://devicetree.org/schemas/display/bridge/lontium,lt9611.yaml#
 $schema: http://devicetree.org/meta-schemas/core.yaml#
 
-title: Lontium LT9611(UXC) 2 Port MIPI to HDMI Bridge
+title: Lontium LT9611(UXC/C/EX/UXD) 2 Port MIPI DSI to HDMI Bridge
 
 maintainers:
   - Vinod Koul <vkoul@kernel.org>
 
 description: |
-  The LT9611 and LT9611UXC are bridge devices which convert DSI to HDMI
+  The LT9611、LT9611UXC、LT9611C、LT9611EX and LT9611UXD
+  are bridge devices which convert DSI to HDMI
 
 properties:
   compatible:
     enum:
       - lontium,lt9611
+      - lontium,lt9611c
+      - lontium,lt9611ex
       - lontium,lt9611uxc
+      - lontium,lt9611uxd
 
   reg:
     maxItems: 1

-- 
2.34.1


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

* [PATCH v8 2/2] drm/bridge: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver
  2026-07-28 11:09 [PATCH v8 0/2] Subject: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver mohit.dsor
  2026-07-28 11:09 ` [PATCH v8 1/2] dt-bindings: bridge: " mohit.dsor
@ 2026-07-28 11:09 ` mohit.dsor
  2026-07-28 11:26   ` sashiko-bot
  1 sibling, 1 reply; 5+ messages in thread
From: mohit.dsor @ 2026-07-28 11:09 UTC (permalink / raw)
  To: Sunyun Yang, Andrzej Hajda, Neil Armstrong, Robert Foss,
	Laurent Pinchart, Jonas Karlman, Jernej Skrabec, Luca Ceresoli,
	Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Vinod Koul
  Cc: dri-devel, devicetree, linux-kernel, Mohit Dsor, venkata.valluru,
	Jessica Zhang

From: Sunyun Yang <syyang@lontium.com>

LT9611C(EX/UXD) is an I2C-controlled chip that Receiver signal/dual port
mipi dsi and output hdmi, differences in hardware features:
- LT9611C: supports 1-port mipi dsi to hdmi 1.4
- LT9611EX: supports 2-port mipi dsi to hdmi 1.4
- LT9611UXD: supports 2-port mipi dsi to hdmi 1.4/2.0

Signed-off-by: Sunyun Yang <syyang@lontium.com>
Co-developed-by: Mohit Dsor <mohit.dsor@oss.qualcomm.com>
Signed-off-by: Mohit Dsor <mohit.dsor@oss.qualcomm.com>
---
 .../ABI/testing/sysfs-driver-lontium-lt9611c       |   11 +
 drivers/gpu/drm/bridge/Kconfig                     |   17 +
 drivers/gpu/drm/bridge/Makefile                    |    1 +
 drivers/gpu/drm/bridge/lontium-lt9611c.c           | 1344 ++++++++++++++++++++
 4 files changed, 1373 insertions(+)

diff --git a/Documentation/ABI/testing/sysfs-driver-lontium-lt9611c b/Documentation/ABI/testing/sysfs-driver-lontium-lt9611c
new file mode 100644
index 000000000000..440e102a8bd8
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-driver-lontium-lt9611c
@@ -0,0 +1,11 @@
+What:		/sys/bus/i2c/drivers/lt9611c/.../lt9611c_firmware
+Date:		July 2026
+KernelVersion:	7.3
+Contact:	Mohit Dsor <mohit.dsor@oss.qualcomm.com>
+Description:
+		Read: Returns the current firmware version loaded on the
+		LT9611C(EX/UXD) chip in hex format (e.g. "0x0105").
+
+		Write: Triggers a firmware upgrade of the LT9611C(EX/UXD)
+		chip using the firmware file Lontium/lt9611c_fw.bin loaded
+		via the firmware loader.
diff --git a/drivers/gpu/drm/bridge/Kconfig b/drivers/gpu/drm/bridge/Kconfig
index 4a57d49b4c6d..bb3caaff8a12 100644
--- a/drivers/gpu/drm/bridge/Kconfig
+++ b/drivers/gpu/drm/bridge/Kconfig
@@ -177,6 +177,23 @@ config DRM_LONTIUM_LT9611
 	  HDMI signals
 	  Please say Y if you have such hardware.
 
+config DRM_LONTIUM_LT9611C
+	tristate "Lontium LT9611C DSI/HDMI bridge"
+	select SND_SOC_HDMI_CODEC if SND_SOC
+	depends on OF
+	select CRC8
+	select FW_LOADER
+	select DRM_PANEL_BRIDGE
+	select DRM_KMS_HELPER
+	select DRM_MIPI_DSI
+	select DRM_DISPLAY_HELPER
+	select DRM_DISPLAY_HDMI_STATE_HELPER
+	select REGMAP_I2C
+	help
+	  Driver for the Lontium LT9611C(EX/UXD) DSI to HDMI bridge chip.
+	  It converts single or dual MIPI DSI and I2S signals to HDMI
+	  output. Say Y if you have hardware using this chip.
+
 config DRM_LONTIUM_LT9611UXC
 	tristate "Lontium LT9611UXC DSI/HDMI bridge"
 	select SND_SOC_HDMI_CODEC if SND_SOC
diff --git a/drivers/gpu/drm/bridge/Makefile b/drivers/gpu/drm/bridge/Makefile
index 15cc821d85b7..f322f3f89f6b 100644
--- a/drivers/gpu/drm/bridge/Makefile
+++ b/drivers/gpu/drm/bridge/Makefile
@@ -16,6 +16,7 @@ obj-$(CONFIG_DRM_ITE_IT6505) += ite-it6505.o
 obj-$(CONFIG_DRM_LONTIUM_LT8912B) += lontium-lt8912b.o
 obj-$(CONFIG_DRM_LONTIUM_LT9211) += lontium-lt9211.o
 obj-$(CONFIG_DRM_LONTIUM_LT9611) += lontium-lt9611.o
+obj-$(CONFIG_DRM_LONTIUM_LT9611C) += lontium-lt9611c.o
 obj-$(CONFIG_DRM_LONTIUM_LT9611UXC) += lontium-lt9611uxc.o
 obj-$(CONFIG_DRM_LONTIUM_LT8713SX) += lontium-lt8713sx.o
 obj-$(CONFIG_DRM_LVDS_CODEC) += lvds-codec.o
diff --git a/drivers/gpu/drm/bridge/lontium-lt9611c.c b/drivers/gpu/drm/bridge/lontium-lt9611c.c
new file mode 100644
index 000000000000..704b829abe57
--- /dev/null
+++ b/drivers/gpu/drm/bridge/lontium-lt9611c.c
@@ -0,0 +1,1344 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2026 Lontium Semiconductor, Inc.
+ * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+ */
+
+#include <linux/crc8.h>
+#include <linux/firmware.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/interrupt.h>
+#include <linux/media-bus-format.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/of_graph.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_bridge.h>
+#include <drm/drm_connector.h>
+#include <drm/drm_drv.h>
+#include <drm/drm_edid.h>
+#include <drm/drm_mipi_dsi.h>
+#include <drm/drm_modes.h>
+#include <drm/drm_of.h>
+#include <drm/drm_print.h>
+#include <drm/drm_probe_helper.h>
+#include <drm/display/drm_hdmi_audio_helper.h>
+#include <drm/display/drm_hdmi_state_helper.h>
+#include <sound/hdmi-codec.h>
+
+#define FW_SIZE (64 * 1024)
+#define LT_PAGE_SIZE 256
+#define FW_FILE  "Lontium/lt9611c_fw.bin"
+#define LT9611C_CRC_POLYNOMIAL 0x31
+#define LT9611C_PAGE_CONTROL 0xff
+#define LT9611C_INFOFRAME_MAX_SIZE 32
+#define LT9611C_CMD_HDR_SIZE 4
+#define LT9611C_CMD_Y0_SIZE  1  /* Y0 echo byte in ACK response */
+#define LT9611C_EDID_BUF_SIZE 32
+
+struct lt9611c_cmd_hdr {
+	u8 func;
+	u8 type;
+	u8 seq;
+	u8 sep;
+};
+
+/* lt9611c_cmd_hdr.func values */
+#define LT9611C_FUNC_WRITE	0x57 /* 'W' */
+#define LT9611C_FUNC_READ	0x52 /* 'R' */
+#define LT9611C_FUNC_ACK	0x41 /* 'A' */
+
+/* lt9611c_cmd_hdr.type values */
+#define LT9611C_TYPE_MIPI	0x4d /* 'M' */
+#define LT9611C_TYPE_LVDS	0x4c /* 'L' */
+#define LT9611C_TYPE_HDMI	0x48 /* 'H' */
+#define LT9611C_TYPE_AUDIO	0x41 /* 'A' */
+#define LT9611C_TYPE_CUSTOM	0x43 /* 'C' */
+
+/* lt9611c_cmd_hdr.sep is always ':' */
+#define LT9611C_CMD_SEP		0x3a /* ':' */
+
+struct lt9611c_cmd {
+	struct lt9611c_cmd_hdr hdr;
+	const u8 *data;
+	size_t data_len;
+};
+
+struct lt9611c_rsp {
+	struct lt9611c_cmd_hdr hdr;
+	u8 *data;
+	unsigned int data_len;
+};
+
+enum lt9611_chip_type {
+	CHIP_LT9611C = 0,
+	CHIP_LT9611EX,
+	CHIP_LT9611UXD,
+};
+
+struct lt9611c {
+	struct device *dev;
+	struct i2c_client *client;
+	struct drm_bridge bridge;
+	struct regmap *regmap;
+	/* Protects all accesses to registers by stopping the on-chip MCU */
+	struct mutex ocm_lock;
+	struct work_struct work;
+	struct device_node *dsi0_node;
+	struct device_node *dsi1_node;
+	struct mipi_dsi_device *dsi0;
+	struct mipi_dsi_device *dsi1;
+	struct gpio_desc *reset_gpio;
+	struct regulator_bulk_data supplies[2];
+	int fw_version;
+	/* Chip variant: C/EX/UXD */
+	enum lt9611_chip_type chip_type;
+	 /* HDMI cable connection status */
+	bool hdmi_connected;
+};
+
+DECLARE_CRC8_TABLE(lt9611c_crc8_table);
+
+static const struct regmap_range_cfg lt9611c_ranges[] = {
+	{
+		.name = "register_range",
+		.range_min =  0,
+		.range_max = 0xfe9c,
+		.selector_reg = LT9611C_PAGE_CONTROL,
+		.selector_mask = 0xff,
+		.selector_shift = 0,
+		.window_start = 0,
+		.window_len = 0x100,
+	},
+};
+
+static const struct regmap_config lt9611c_regmap_config = {
+	.reg_bits = 8,
+	.val_bits = 8,
+	.max_register = 0xfe9c,
+	.ranges = lt9611c_ranges,
+	.num_ranges = ARRAY_SIZE(lt9611c_ranges),
+};
+
+static int lt9611c_read_write_flow(struct lt9611c *lt9611c,
+				   const struct lt9611c_cmd *cmd,
+				   struct lt9611c_rsp *rsp)
+{
+	int ret;
+	unsigned int i;
+	unsigned int temp;
+	unsigned int max_params = 0xe0dd - 0xe0b0 + 1;
+
+	regmap_write(lt9611c->regmap, 0xe0de, 0x01);
+
+	ret = regmap_read_poll_timeout(lt9611c->regmap, 0xe0ae, temp,
+				       temp == 0x01, 1000, 200 * 1000);
+	if (ret)
+		return -ETIMEDOUT;
+
+	regmap_write(lt9611c->regmap, 0xe0b0 + 0, cmd->hdr.func);
+	regmap_write(lt9611c->regmap, 0xe0b0 + 1, cmd->hdr.type);
+	regmap_write(lt9611c->regmap, 0xe0b0 + 2, cmd->hdr.seq);
+	regmap_write(lt9611c->regmap, 0xe0b0 + 3, cmd->hdr.sep);
+
+	for (i = 0; cmd->data && i < cmd->data_len &&
+	     (LT9611C_CMD_HDR_SIZE + i) < max_params; i++)
+		regmap_write(lt9611c->regmap,
+			     0xe0b0 + LT9611C_CMD_HDR_SIZE + i, cmd->data[i]);
+
+	regmap_write(lt9611c->regmap, 0xe0de, 0x02);
+
+	ret = regmap_read_poll_timeout(lt9611c->regmap, 0xe0ae, temp,
+				       temp == 0x02, 1000, 200 * 1000);
+	if (ret)
+		return -ETIMEDOUT;
+
+	ret = regmap_bulk_read(lt9611c->regmap, 0xe085,
+			       &rsp->hdr, LT9611C_CMD_HDR_SIZE);
+	if (ret)
+		return ret;
+
+
+	if (rsp->data && rsp->data_len)
+		ret = regmap_bulk_read(lt9611c->regmap,
+				       0xe085 + LT9611C_CMD_HDR_SIZE,
+				       rsp->data, rsp->data_len);
+
+	return ret;
+}
+
+static void lt9611c_config_parameters(struct lt9611c *lt9611c)
+{
+	const struct reg_sequence seq_write_paras[] = {
+		REG_SEQ0(0xe0ee, 0x01),
+		REG_SEQ0(0xe103, 0x3f), /*fifo rst*/
+		REG_SEQ0(0xe103, 0xff),
+		REG_SEQ0(0xe05e, 0xc1),
+		REG_SEQ0(0xe058, 0x00),
+		REG_SEQ0(0xe059, 0x50),
+		REG_SEQ0(0xe05a, 0x10),
+		REG_SEQ0(0xe05a, 0x00),
+		REG_SEQ0(0xe058, 0x21),
+	};
+
+	regmap_multi_reg_write(lt9611c->regmap, seq_write_paras, ARRAY_SIZE(seq_write_paras));
+}
+
+static void lt9611c_wren(struct lt9611c *lt9611c)
+{
+	regmap_write(lt9611c->regmap, 0xe05a, 0x04);
+	regmap_write(lt9611c->regmap, 0xe05a, 0x00);
+}
+
+static void lt9611c_wrdi(struct lt9611c *lt9611c)
+{
+	regmap_write(lt9611c->regmap, 0xe05a, 0x08);
+	regmap_write(lt9611c->regmap, 0xe05a, 0x00);
+}
+
+static void lt9611c_erase_op(struct lt9611c *lt9611c, u32 addr)
+{
+	const struct reg_sequence seq_write[] = {
+		REG_SEQ0(0xe0ee, 0x01),
+		REG_SEQ0(0xe05a, 0x04),
+		REG_SEQ0(0xe05a, 0x00),
+		REG_SEQ0(0xe05b, (addr >> 16) & 0xff),
+		REG_SEQ0(0xe05c, (addr >> 8) & 0xff),
+		REG_SEQ0(0xe05d, addr & 0xff),
+		REG_SEQ0(0xe05a, 0x01),
+		REG_SEQ0(0xe05a, 0x00),
+	};
+
+	regmap_multi_reg_write(lt9611c->regmap, seq_write, ARRAY_SIZE(seq_write));
+}
+
+static void read_flash_reg_status(struct lt9611c *lt9611c, unsigned int *status)
+{
+	const struct reg_sequence seq_write[] = {
+		REG_SEQ0(0xe103, 0x3f),
+		REG_SEQ0(0xe103, 0xff),
+		REG_SEQ0(0xe05e, 0x40),
+		REG_SEQ0(0xe056, 0x05),
+		REG_SEQ0(0xe055, 0x25),
+		REG_SEQ0(0xe055, 0x01),
+		REG_SEQ0(0xe058, 0x21),
+	};
+
+	regmap_multi_reg_write(lt9611c->regmap, seq_write, ARRAY_SIZE(seq_write));
+
+	regmap_read(lt9611c->regmap, 0xe05f, status);
+}
+
+static void lt9611c_crc_to_sram(struct lt9611c *lt9611c)
+{
+	const struct reg_sequence seq_write[] = {
+		REG_SEQ0(0xe051, 0x00),
+		REG_SEQ0(0xe055, 0xc0),
+		REG_SEQ0(0xe055, 0x80),
+		REG_SEQ0(0xe05e, 0xc0),
+		REG_SEQ0(0xe058, 0x21),
+	};
+
+	regmap_multi_reg_write(lt9611c->regmap, seq_write, ARRAY_SIZE(seq_write));
+}
+
+static void lt9611c_data_to_sram(struct lt9611c *lt9611c)
+{
+	const struct reg_sequence seq_write[] = {
+		REG_SEQ0(0xe051, 0xff),
+		REG_SEQ0(0xe055, 0x80),
+		REG_SEQ0(0xe05e, 0xc0),
+		REG_SEQ0(0xe058, 0x21),
+	};
+
+	regmap_multi_reg_write(lt9611c->regmap, seq_write, ARRAY_SIZE(seq_write));
+}
+
+static void lt9611c_sram_to_flash(struct lt9611c *lt9611c, size_t addr)
+{
+	const struct reg_sequence seq_write[] = {
+		REG_SEQ0(0xe05b, (addr >> 16) & 0xff),
+		REG_SEQ0(0xe05c, (addr >> 8) & 0xff),
+		REG_SEQ0(0xe05d, addr & 0xff),
+		REG_SEQ0(0xe05a, 0x30),
+		REG_SEQ0(0xe05a, 0x00),
+	};
+
+	regmap_multi_reg_write(lt9611c->regmap, seq_write, ARRAY_SIZE(seq_write));
+}
+
+static void lt9611c_block_erase(struct lt9611c *lt9611c)
+{
+	int i;
+	unsigned int block_num;
+	unsigned int flash_status = 0;
+	u32 flash_addr = 0;
+
+	for (block_num = 0; block_num < 2; block_num++) {
+		flash_addr = (block_num * 0x008000);
+		lt9611c_erase_op(lt9611c, flash_addr);
+		msleep(100);
+		i = 0;
+		while (1) {
+			read_flash_reg_status(lt9611c, &flash_status);
+			if ((flash_status & 0x01) == 0)
+				break;
+
+			if (i > 50)
+				break;
+
+			i++;
+			msleep(50);
+		}
+	}
+}
+
+static int lt9611c_write_data(struct lt9611c *lt9611c, const struct firmware *fw, size_t addr)
+{
+	struct device *dev = lt9611c->dev;
+	int ret;
+	unsigned int page = 0, num = 0, i = 0;
+	size_t size, index;
+	const u8 *data;
+	u8 value;
+
+	data = fw->data;
+	size = fw->size;
+	page = (size + LT_PAGE_SIZE - 1) / LT_PAGE_SIZE;
+	if (page * LT_PAGE_SIZE > FW_SIZE) {
+		dev_err(dev, "firmware size out of range\n");
+		return -EINVAL;
+	}
+
+	dev_dbg(dev, "%u pages, total size %zu byte\n", page, size);
+
+	for (num = 0; num < page; num++) {
+		lt9611c_data_to_sram(lt9611c);
+
+		for (i = 0; i < LT_PAGE_SIZE; i++) {
+			index = num * LT_PAGE_SIZE + i;
+			value = (index < size) ? data[index] : 0xff;
+
+			ret = regmap_write(lt9611c->regmap, 0xe059, value);
+			if (ret < 0) {
+				dev_err(dev, "write error at page %u, index %u\n", num, i);
+				return ret;
+			}
+		}
+
+		lt9611c_wren(lt9611c);
+		lt9611c_sram_to_flash(lt9611c, addr);
+
+		addr += LT_PAGE_SIZE;
+	}
+
+	lt9611c_wrdi(lt9611c);
+
+	return 0;
+}
+
+static int lt9611c_write_crc(struct lt9611c *lt9611c, u8 fw_crc, size_t addr)
+{
+	struct device *dev = lt9611c->dev;
+	int ret;
+
+	lt9611c_crc_to_sram(lt9611c);
+	ret = regmap_write(lt9611c->regmap, 0xe059, fw_crc);
+	if (ret < 0) {
+		dev_err(dev, "failed to write crc\n");
+		return ret;
+	}
+
+	lt9611c_wren(lt9611c);
+	lt9611c_sram_to_flash(lt9611c, addr);
+	lt9611c_wrdi(lt9611c);
+
+	dev_dbg(dev, "crc 0x%02x written to flash at addr 0x%zx\n", fw_crc, addr);
+
+	return 0;
+}
+
+static void lt9611c_reset(struct lt9611c *lt9611c)
+{
+	gpiod_set_value_cansleep(lt9611c->reset_gpio, 1);
+	usleep_range(10000, 12000);
+
+	gpiod_set_value_cansleep(lt9611c->reset_gpio, 0);
+	msleep(400);
+}
+
+static int lt9611c_upgrade_result(struct lt9611c *lt9611c, u8 fw_crc)
+{
+	struct device *dev = lt9611c->dev;
+	unsigned int crc_result;
+
+	regmap_write(lt9611c->regmap, 0xe0ee, 0x01);
+	regmap_read(lt9611c->regmap, 0xe021, &crc_result);
+
+	if (crc_result != fw_crc) {
+		dev_err(dev, "lt9611c fw upgrade failed, expected crc=0x%02x, read crc=0x%02x\n",
+			fw_crc, crc_result);
+		return -1;
+	}
+
+	dev_dbg(dev, "lt9611c firmware upgrade success, crc=0x%02x\n", crc_result);
+	return 0;
+}
+
+static int lt9611c_firmware_upgrade(struct lt9611c *lt9611c)
+{
+	struct device *dev = lt9611c->dev;
+	const struct firmware *fw;
+	u8 *buffer;
+	size_t total_size = FW_SIZE - 1;
+	u8 fw_crc;
+	int ret;
+
+	/* 1. load firmware */
+	ret = request_firmware(&fw, FW_FILE, dev);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to load '%s'\n", FW_FILE);
+
+	/* 2. check size */
+	if (fw->size > total_size) {
+		dev_err(dev, "firmware too large (%zu > %zu)\n", fw->size, total_size);
+		ret = -EINVAL;
+		goto out_release_fw;
+	}
+	dev_dbg(dev, "firmware size: %zu bytes\n", fw->size);
+
+	/* 3. calculate crc8 */
+	buffer = kzalloc(total_size, GFP_KERNEL);
+	if (!buffer) {
+		ret = -ENOMEM;
+		goto out_release_fw;
+	}
+
+	memcpy(buffer, fw->data, fw->size);
+	memset(buffer + fw->size, 0xff, total_size - fw->size);
+
+	fw_crc = crc8(lt9611c_crc8_table, buffer, total_size, 0);
+	kfree(buffer);
+
+	dev_info(dev, "starting firmware upgrade, size: %zu bytes, crc: 0x%02x\n",
+		 fw->size, fw_crc);
+
+	/* 4. firmware upgrade */
+	lt9611c_config_parameters(lt9611c);
+	lt9611c_block_erase(lt9611c);
+
+	ret = lt9611c_write_data(lt9611c, fw, 0);
+	if (ret < 0) {
+		dev_err(dev, "failed to write firmware data\n");
+		goto out_release_fw;
+	}
+
+	ret = lt9611c_write_crc(lt9611c, fw_crc, FW_SIZE - 1);
+	if (ret < 0) {
+		dev_err(dev, "failed to write firmware crc\n");
+		goto out_release_fw;
+	}
+
+	/* 5. check upgrade of result */
+	lt9611c_reset(lt9611c);
+	ret = lt9611c_upgrade_result(lt9611c, fw_crc);
+
+out_release_fw:
+	release_firmware(fw);
+	return ret;
+}
+
+static struct lt9611c *bridge_to_lt9611c(struct drm_bridge *bridge)
+{
+	return container_of(bridge, struct lt9611c, bridge);
+}
+
+static const struct lt9611c *bridge_to_lt9611c_const(const struct drm_bridge *bridge)
+{
+	return container_of_const(bridge, struct lt9611c, bridge);
+}
+
+static void lt9611c_lock(struct lt9611c *lt9611c)
+{
+	mutex_lock(&lt9611c->ocm_lock);
+	regmap_write(lt9611c->regmap, 0xe0ee, 0x01);
+}
+
+static void lt9611c_unlock(struct lt9611c *lt9611c)
+{
+	regmap_write(lt9611c->regmap, 0xe0ee, 0x00);
+	mutex_unlock(&lt9611c->ocm_lock);
+}
+
+static irqreturn_t lt9611c_irq_thread_handler(int irq, void *dev_id)
+{
+	struct lt9611c *lt9611c = dev_id;
+	struct device *dev = lt9611c->dev;
+	int ret;
+	unsigned int irq_status;
+
+	guard(mutex)(&lt9611c->ocm_lock);
+
+	ret = regmap_read(lt9611c->regmap, 0xe084, &irq_status);
+	if (ret) {
+		dev_err(dev, "failed to read irq status: %d\n", ret);
+		return IRQ_HANDLED;
+	}
+
+	if (!(irq_status & BIT(0)))
+		return IRQ_HANDLED;
+
+	/*Clear interrupt: hardware requires two writes with delay*/
+	regmap_write(lt9611c->regmap, 0xe0df, irq_status & BIT(0));
+	usleep_range(10000, 12000);
+	regmap_write(lt9611c->regmap, 0xe0df, irq_status & (~BIT(0)));
+
+	schedule_work(&lt9611c->work);
+
+	return IRQ_HANDLED;
+}
+
+static void lt9611c_hpd_work(struct work_struct *work)
+{
+	struct lt9611c *lt9611c = container_of(work, struct lt9611c, work);
+	struct device *dev = lt9611c->dev;
+	static const u8 hpd_data[] = { 0x00 };
+	struct lt9611c_cmd cmd = {
+		.hdr = { LT9611C_FUNC_READ, LT9611C_TYPE_HDMI, 0x31, LT9611C_CMD_SEP },
+		.data = hpd_data,
+		.data_len = 1,
+	};
+	u8 hpd_status;
+	struct lt9611c_rsp rsp = { .data = &hpd_status, .data_len = 1 };
+	bool connected;
+	int ret;
+
+	/* Added delay as need time to reflect hpd after interrupt*/
+	msleep(200);
+
+	mutex_lock(&lt9611c->ocm_lock);
+	ret = lt9611c_read_write_flow(lt9611c, &cmd, &rsp);
+	if (ret) {
+		dev_err(dev, "failed to read HPD status\n");
+	} else {
+		lt9611c->hdmi_connected = (hpd_status == 0x02);
+	}
+	connected = lt9611c->hdmi_connected;
+	mutex_unlock(&lt9611c->ocm_lock);
+
+	drm_bridge_hpd_notify(&lt9611c->bridge,
+			      connected ? connector_status_connected :
+			      connector_status_disconnected);
+}
+
+static int lt9611c_regulator_init(struct lt9611c *lt9611c)
+{
+	struct device *dev = lt9611c->dev;
+	int ret;
+
+	lt9611c->supplies[0].supply = "vcc";
+	lt9611c->supplies[1].supply = "vdd";
+
+	ret = devm_regulator_bulk_get(dev, 2, lt9611c->supplies);
+
+	return ret;
+}
+
+static struct mipi_dsi_device *lt9611c_attach_dsi(struct lt9611c *lt9611c,
+						  struct device_node *dsi_node)
+{
+	const struct mipi_dsi_device_info info = { "lt9611c", 0, NULL };
+	struct mipi_dsi_device *dsi;
+	struct mipi_dsi_host *host;
+	struct device *dev = lt9611c->dev;
+	int ret;
+
+	host = of_find_mipi_dsi_host_by_node(dsi_node);
+	if (!host)
+		return ERR_PTR(dev_err_probe(dev, -EPROBE_DEFER, "failed to find dsi host\n"));
+
+	dsi = devm_mipi_dsi_device_register_full(dev, host, &info);
+	if (IS_ERR(dsi))
+		return ERR_PTR(dev_err_probe(dev, PTR_ERR(dsi), "failed to create dsi device\n"));
+
+	dsi->lanes = 4;
+	dsi->format = MIPI_DSI_FMT_RGB888;
+	dsi->mode_flags = MIPI_DSI_MODE_VIDEO | MIPI_DSI_MODE_VIDEO_SYNC_PULSE |
+			 MIPI_DSI_MODE_VIDEO_HSE;
+
+	ret = devm_mipi_dsi_attach(dev, dsi);
+	if (ret < 0)
+		return ERR_PTR(dev_err_probe(dev, ret, "failed to attach dsi to host\n"));
+
+	return dsi;
+}
+
+static int lt9611c_bridge_attach(struct drm_bridge *bridge,
+				 struct drm_encoder *encoder,
+				 enum drm_bridge_attach_flags flags)
+{
+	struct lt9611c *lt9611c = bridge_to_lt9611c(bridge);
+
+	return drm_bridge_attach(encoder, lt9611c->bridge.next_bridge, bridge, flags);
+}
+
+static enum drm_mode_status
+lt9611c_hdmi_tmds_char_rate_valid(const struct drm_bridge *bridge,
+				  const struct drm_display_mode *mode,
+				  unsigned long long tmds_rate)
+{
+	const struct lt9611c *lt9611c = bridge_to_lt9611c_const(bridge);
+
+	if (lt9611c->chip_type == CHIP_LT9611UXD) {
+		if (tmds_rate > 600000000)
+			return MODE_CLOCK_HIGH;
+
+	} else {
+		if (tmds_rate > 340000000)
+			return MODE_CLOCK_HIGH;
+	}
+
+	if (tmds_rate < 25000000)
+		return MODE_CLOCK_LOW;
+
+	return MODE_OK;
+}
+
+static void lt9611c_video_setup(struct lt9611c *lt9611c,
+				const struct drm_display_mode *mode)
+{
+	struct device *dev = lt9611c->dev;
+	int ret;
+	u32 h_total, hactive, hsync_len, hfront_porch, hback_porch;
+	u32 v_total, vactive, vsync_len, vfront_porch, vback_porch;
+	u8 timing_data[22];
+	struct lt9611c_rsp rsp = {};
+	u8 framerate;
+	u8 vic = 0x00;
+	struct lt9611c_cmd cmd = {
+		.hdr = { LT9611C_FUNC_WRITE, LT9611C_TYPE_MIPI, 0x33, LT9611C_CMD_SEP },
+		.data = timing_data,
+		.data_len = ARRAY_SIZE(timing_data),
+	};
+
+	guard(mutex)(&lt9611c->ocm_lock);
+	h_total = mode->htotal;
+	hactive = mode->hdisplay;
+	hsync_len = mode->hsync_end - mode->hsync_start;
+	hfront_porch = mode->hsync_start - mode->hdisplay;
+	hback_porch = mode->htotal - mode->hsync_end;
+
+	v_total = mode->vtotal;
+	vactive = mode->vdisplay;
+	vsync_len = mode->vsync_end - mode->vsync_start;
+	vfront_porch = mode->vsync_start - mode->vdisplay;
+	vback_porch = mode->vtotal - mode->vsync_end;
+	framerate = drm_mode_vrefresh(mode);
+	vic = drm_match_cea_mode(mode);
+
+	dev_dbg(dev, "hactive=%d, vactive=%d\n", hactive, vactive);
+	dev_dbg(dev, "framerate=%d\n", framerate);
+	dev_dbg(dev, "vic = 0x%02x\n", vic);
+
+	timing_data[0] = (h_total >> 8) & 0xff;
+	timing_data[1] = h_total & 0xff;
+	timing_data[2] = (hactive >> 8) & 0xff;
+	timing_data[3] = hactive & 0xff;
+	timing_data[4] = (hfront_porch >> 8) & 0xff;
+	timing_data[5] = hfront_porch & 0xff;
+	timing_data[6] = (hsync_len >> 8) & 0xff;
+	timing_data[7] = hsync_len & 0xff;
+	timing_data[8] = (hback_porch >> 8) & 0xff;
+	timing_data[9] = hback_porch & 0xff;
+	timing_data[10] = (v_total >> 8) & 0xff;
+	timing_data[11] = v_total & 0xff;
+	timing_data[12] = (vactive >> 8) & 0xff;
+	timing_data[13] = vactive & 0xFF;
+	timing_data[14] = (vfront_porch >> 8) & 0xff;
+	timing_data[15] = vfront_porch & 0xff;
+	timing_data[16] = (vsync_len >> 8) & 0xff;
+	timing_data[17] = vsync_len & 0xff;
+	timing_data[18] = (vback_porch >> 8) & 0xff;
+	timing_data[19] = vback_porch & 0xff;
+	timing_data[20] = framerate;
+	timing_data[21] = vic;
+
+	ret = lt9611c_read_write_flow(lt9611c, &cmd, &rsp);
+	if (ret)
+		dev_err(dev, "video set failed\n");
+}
+
+static void lt9611c_bridge_atomic_enable(struct drm_bridge *bridge,
+					 struct drm_atomic_commit *state)
+{
+	struct lt9611c *lt9611c = bridge_to_lt9611c(bridge);
+	struct drm_connector *connector;
+	struct drm_connector_state *conn_state;
+	struct drm_crtc_state *crtc_state;
+	struct drm_display_mode *mode;
+
+	connector = drm_atomic_get_new_connector_for_encoder(state, bridge->encoder);
+	if (WARN_ON(!connector))
+		return;
+
+	conn_state = drm_atomic_get_new_connector_state(state, connector);
+	if (WARN_ON(!conn_state))
+		return;
+
+	crtc_state = drm_atomic_get_new_crtc_state(state, conn_state->crtc);
+	if (WARN_ON(!crtc_state))
+		return;
+
+	mode = &crtc_state->adjusted_mode;
+
+	lt9611c_video_setup(lt9611c, mode);
+}
+
+static enum drm_connector_status
+lt9611c_bridge_detect(struct drm_bridge *bridge, struct drm_connector *connector)
+{
+	struct lt9611c *lt9611c = bridge_to_lt9611c(bridge);
+	struct device *dev = lt9611c->dev;
+	int ret;
+	bool connected = false;
+	static const u8 hpd_data[] = { 0x00 };
+	struct lt9611c_cmd cmd = {
+		.hdr = { LT9611C_FUNC_READ, LT9611C_TYPE_HDMI, 0x31, LT9611C_CMD_SEP },
+		.data = hpd_data,
+		.data_len = 1,
+	};
+	u8 hpd_status;
+	struct lt9611c_rsp rsp = { .data = &hpd_status, .data_len = 1 };
+
+	guard(mutex)(&lt9611c->ocm_lock);
+
+	ret = lt9611c_read_write_flow(lt9611c, &cmd, &rsp);
+	if (ret) {
+		dev_err(dev, "failed to read HPD status (err=%d)\n", ret);
+	} else {
+		connected = (hpd_status == 0x02);
+	}
+
+	lt9611c->hdmi_connected = connected;
+
+	return connected ? connector_status_connected :
+				connector_status_disconnected;
+}
+
+static int lt9611c_get_edid_block(void *data, u8 *buf,
+				  unsigned int block, size_t len)
+{
+	struct lt9611c *lt9611c = data;
+	struct device *dev = lt9611c->dev;
+	u8 edid_raw[LT9611C_CMD_Y0_SIZE + LT9611C_EDID_BUF_SIZE];
+	u8 y0;
+	int ret, i, offset = 0;
+	struct lt9611c_cmd cmd = {
+		.hdr = { LT9611C_FUNC_READ, LT9611C_TYPE_HDMI, 0x33, LT9611C_CMD_SEP },
+	};
+	struct lt9611c_rsp rsp = {
+		.data = edid_raw,
+		.data_len = LT9611C_CMD_Y0_SIZE + LT9611C_EDID_BUF_SIZE,
+	};
+
+	if (len != 128)
+		return -EINVAL;
+	guard(mutex)(&lt9611c->ocm_lock);
+
+	for (i = 0; i < 4; i++) {
+		y0 = block * 4 + i;
+		cmd.data = &y0;
+		cmd.data_len = 1;
+		ret = lt9611c_read_write_flow(lt9611c, &cmd, &rsp);
+		if (ret) {
+			dev_err(dev, "Failed to read EDID block %u packet %d\n",
+				block, i);
+			return ret;
+		}
+		memcpy(buf + offset, &edid_raw[LT9611C_CMD_Y0_SIZE], LT9611C_EDID_BUF_SIZE);
+		offset += LT9611C_EDID_BUF_SIZE;
+	}
+
+	return 0;
+}
+
+static const struct drm_edid *lt9611c_bridge_edid_read(struct drm_bridge *bridge,
+						       struct drm_connector *connector)
+{
+	struct lt9611c *lt9611c = bridge_to_lt9611c(bridge);
+
+	return drm_edid_read_custom(connector, lt9611c_get_edid_block, lt9611c);
+}
+
+static int lt9611c_hdmi_write_avi_infoframe(struct drm_bridge *bridge,
+					    const u8 *buffer, size_t len)
+{
+	struct lt9611c *lt9611c = bridge_to_lt9611c(bridge);
+	u8 extra[1 + LT9611C_INFOFRAME_MAX_SIZE];
+	struct lt9611c_rsp rsp = {};
+	struct lt9611c_cmd cmd = {
+		.hdr = { LT9611C_FUNC_WRITE, LT9611C_TYPE_HDMI, 0x35, LT9611C_CMD_SEP },
+	};
+
+	if (WARN_ON(len > LT9611C_INFOFRAME_MAX_SIZE))
+		return -EINVAL;
+
+	extra[0] = 0x01; /* write avi */
+	memcpy(&extra[1], buffer, len);
+	cmd.data = extra;
+	cmd.data_len = 1 + len;
+
+	guard(mutex)(&lt9611c->ocm_lock);
+
+	return lt9611c_read_write_flow(lt9611c, &cmd, &rsp);
+}
+
+static int lt9611c_hdmi_clear_avi_infoframe(struct drm_bridge *bridge)
+{
+	struct lt9611c *lt9611c = bridge_to_lt9611c(bridge);
+	static const u8 clear_data[] = { 0x01 };
+	struct lt9611c_cmd cmd = {
+		.hdr = { LT9611C_FUNC_WRITE, LT9611C_TYPE_HDMI, 0x42, LT9611C_CMD_SEP },
+				   .data = clear_data, .data_len = 1 };
+	struct lt9611c_rsp rsp = {};
+	int ret;
+
+	guard(mutex)(&lt9611c->ocm_lock);
+
+	ret = lt9611c_read_write_flow(lt9611c, &cmd, &rsp);
+
+	if (ret < 0) {
+		dev_err(lt9611c->dev, "clear avi infoframe failed!\n");
+		return ret;
+	}
+
+	return 0;
+}
+
+static int lt9611c_hdmi_write_hdmi_infoframe(struct drm_bridge *bridge,
+					     const u8 *buffer, size_t len)
+{
+	struct lt9611c *lt9611c = bridge_to_lt9611c(bridge);
+	u8 extra[1 + LT9611C_INFOFRAME_MAX_SIZE];
+	struct lt9611c_rsp rsp = {};
+	struct lt9611c_cmd cmd = {
+		.hdr = { LT9611C_FUNC_WRITE, LT9611C_TYPE_HDMI, 0x35, LT9611C_CMD_SEP },
+	};
+
+	if (WARN_ON(len > LT9611C_INFOFRAME_MAX_SIZE))
+		return -EINVAL;
+
+	extra[0] = 0x04; /* write vsif */
+	memcpy(&extra[1], buffer, len);
+	cmd.data = extra;
+	cmd.data_len = 1 + len;
+
+	guard(mutex)(&lt9611c->ocm_lock);
+
+	return lt9611c_read_write_flow(lt9611c, &cmd, &rsp);
+}
+
+static int lt9611c_hdmi_clear_hdmi_infoframe(struct drm_bridge *bridge)
+{
+	struct lt9611c *lt9611c = bridge_to_lt9611c(bridge);
+	static const u8 clear_data[] = { 0x04 };
+	struct lt9611c_cmd cmd = {
+		.hdr = { LT9611C_FUNC_WRITE, LT9611C_TYPE_HDMI, 0x42, LT9611C_CMD_SEP },
+				   .data = clear_data, .data_len = 1 };
+	struct lt9611c_rsp rsp = {};
+	int ret;
+
+	guard(mutex)(&lt9611c->ocm_lock);
+
+	ret = lt9611c_read_write_flow(lt9611c, &cmd, &rsp);
+
+	if (ret < 0) {
+		dev_err(lt9611c->dev, "clear hdmi infoframe failed!\n");
+		return ret;
+	}
+
+	return 0;
+}
+
+static int lt9611c_hdmi_write_audio_infoframe(struct drm_bridge *bridge,
+					      const u8 *buffer, size_t len)
+{
+	struct lt9611c *lt9611c = bridge_to_lt9611c(bridge);
+	u8 extra[1 + LT9611C_INFOFRAME_MAX_SIZE];
+	struct lt9611c_rsp rsp = {};
+	struct lt9611c_cmd cmd = {
+		.hdr = { LT9611C_FUNC_WRITE, LT9611C_TYPE_HDMI, 0x35, LT9611C_CMD_SEP },
+	};
+
+	extra[0] = 0x02; /* write audio */
+	memcpy(&extra[1], buffer, len);
+	cmd.data = extra;
+	cmd.data_len = 1 + len;
+
+	guard(mutex)(&lt9611c->ocm_lock);
+
+	return lt9611c_read_write_flow(lt9611c, &cmd, &rsp);
+}
+
+static int lt9611c_hdmi_clear_audio_infoframe(struct drm_bridge *bridge)
+{
+	struct lt9611c *lt9611c = bridge_to_lt9611c(bridge);
+	static const u8 clear_data[] = { 0x02 };
+	struct lt9611c_cmd cmd = {
+		.hdr = { LT9611C_FUNC_WRITE, LT9611C_TYPE_HDMI, 0x42, LT9611C_CMD_SEP },
+				   .data = clear_data, .data_len = 1 };
+	struct lt9611c_rsp rsp = {};
+	int ret;
+
+	guard(mutex)(&lt9611c->ocm_lock);
+
+	ret = lt9611c_read_write_flow(lt9611c, &cmd, &rsp);
+
+	if (ret < 0) {
+		dev_err(lt9611c->dev, "clear audio infoframe failed!\n");
+		return ret;
+	}
+
+	return 0;
+}
+
+static int lt9611c_hdmi_audio_prepare(struct drm_bridge *bridge,
+				      struct drm_connector *connector,
+				      struct hdmi_codec_daifmt *fmt,
+				      struct hdmi_codec_params *hparms)
+{
+	struct lt9611c *lt9611c = bridge_to_lt9611c(bridge);
+	u8 audio_extra[2];
+	struct lt9611c_rsp rsp = {};
+	int ret;
+	struct lt9611c_cmd cmd = {
+		.hdr = { LT9611C_FUNC_WRITE, LT9611C_TYPE_HDMI, 0x36, LT9611C_CMD_SEP },
+				   .data = audio_extra,
+				   .data_len = ARRAY_SIZE(audio_extra) };
+
+	if (hparms->sample_width == 32)
+		return -EINVAL;
+
+	switch (fmt->fmt) {
+	case HDMI_I2S:
+		audio_extra[0] = 0x01;
+		break;
+	case HDMI_SPDIF:
+		audio_extra[0] = 0x02;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	audio_extra[1] = hparms->channels;
+	guard(mutex)(&lt9611c->ocm_lock);
+
+	ret = lt9611c_read_write_flow(lt9611c, &cmd, &rsp);
+	if (ret < 0) {
+		dev_err(lt9611c->dev, "set audio info failed!\n");
+		return ret;
+	}
+
+	return drm_atomic_helper_connector_hdmi_update_audio_infoframe(connector,
+									&hparms->cea);
+}
+
+static void lt9611c_hdmi_audio_shutdown(struct drm_bridge *bridge,
+					struct drm_connector *connector)
+{
+	drm_atomic_helper_connector_hdmi_clear_audio_infoframe(connector);
+}
+
+static void lt9611c_bridge_hpd_enable(struct drm_bridge *bridge)
+{
+	struct lt9611c *lt9611c = bridge_to_lt9611c(bridge);
+	static const u8 hpd_data[] = { 0x00 };
+	struct lt9611c_cmd cmd = {
+		.hdr = { LT9611C_FUNC_READ, LT9611C_TYPE_HDMI, 0x31, LT9611C_CMD_SEP },
+		.data = hpd_data,
+		.data_len = 1,
+	};
+	u8 hpd_status;
+	struct lt9611c_rsp rsp = { .data = &hpd_status, .data_len = 1 };
+	int ret;
+
+	mutex_lock(&lt9611c->ocm_lock);
+	ret = lt9611c_read_write_flow(lt9611c, &cmd, &rsp);
+	if (!ret)
+		lt9611c->hdmi_connected = (hpd_status == 0x02);
+	mutex_unlock(&lt9611c->ocm_lock);
+
+	schedule_work(&lt9611c->work);
+}
+
+static int lt9611c_hdmi_audio_startup(struct drm_bridge *bridge,
+				      struct drm_connector *connector)
+{
+	return 0;
+}
+
+static const struct drm_bridge_funcs lt9611c_bridge_funcs = {
+	.attach = lt9611c_bridge_attach,
+	.detect = lt9611c_bridge_detect,
+	.edid_read = lt9611c_bridge_edid_read,
+	.atomic_enable = lt9611c_bridge_atomic_enable,
+	.atomic_duplicate_state = drm_atomic_helper_bridge_duplicate_state,
+	.atomic_destroy_state = drm_atomic_helper_bridge_destroy_state,
+	.atomic_create_state = drm_atomic_helper_bridge_create_state,
+	.hpd_enable = lt9611c_bridge_hpd_enable,
+
+	.hdmi_tmds_char_rate_valid = lt9611c_hdmi_tmds_char_rate_valid,
+	.hdmi_write_avi_infoframe = lt9611c_hdmi_write_avi_infoframe,
+	.hdmi_clear_avi_infoframe = lt9611c_hdmi_clear_avi_infoframe,
+	.hdmi_write_hdmi_infoframe = lt9611c_hdmi_write_hdmi_infoframe,
+	.hdmi_clear_hdmi_infoframe = lt9611c_hdmi_clear_hdmi_infoframe,
+	.hdmi_write_audio_infoframe = lt9611c_hdmi_write_audio_infoframe,
+	.hdmi_clear_audio_infoframe = lt9611c_hdmi_clear_audio_infoframe,
+
+	.hdmi_audio_startup = lt9611c_hdmi_audio_startup,
+	.hdmi_audio_prepare = lt9611c_hdmi_audio_prepare,
+	.hdmi_audio_shutdown = lt9611c_hdmi_audio_shutdown,
+};
+
+static int lt9611c_parse_dt(struct device *dev,
+			    struct lt9611c *lt9611c)
+{
+	lt9611c->dsi0_node = of_graph_get_remote_node(dev->of_node, 0, -1);
+	if (!lt9611c->dsi0_node)
+		return dev_err_probe(dev, -ENODEV, "failed to get remote node for primary dsi\n");
+
+	lt9611c->dsi1_node = of_graph_get_remote_node(dev->of_node, 1, -1);
+
+	if (lt9611c->dsi1_node && lt9611c->chip_type == CHIP_LT9611C) {
+		of_node_put(lt9611c->dsi1_node);
+		of_node_put(lt9611c->dsi0_node);
+		return dev_err_probe(dev, -EINVAL,
+				     "LT9611C does not support dual DSI\n");
+	}
+
+	lt9611c->bridge.next_bridge = of_drm_get_bridge_by_endpoint(dev->of_node, 2, -1);
+	if (IS_ERR(lt9611c->bridge.next_bridge)) {
+		of_node_put(lt9611c->dsi1_node);
+		of_node_put(lt9611c->dsi0_node);
+		return PTR_ERR(lt9611c->bridge.next_bridge);
+	}
+
+	return 0;
+}
+
+static int lt9611c_read_version(struct lt9611c *lt9611c)
+{
+	u8 buf[2];
+	int ret;
+
+	ret = regmap_write(lt9611c->regmap, 0xe0ee, 0x01);
+	if (ret)
+		return ret;
+
+	ret = regmap_bulk_read(lt9611c->regmap, 0xe080, buf, ARRAY_SIZE(buf));
+	if (ret)
+		return ret;
+
+	return (buf[0] << 8) | buf[1];
+}
+
+static int lt9611c_read_chipid(struct lt9611c *lt9611c)
+{
+	struct device *dev = lt9611c->dev;
+	u8 chipid[2];
+	int ret;
+
+	ret = regmap_write(lt9611c->regmap, 0xe0ee, 0x01);
+	if (ret)
+		return ret;
+
+	ret = regmap_bulk_read(lt9611c->regmap, 0xe100, chipid, 2);
+	if (ret)
+		return ret;
+
+	if (chipid[0] != 0x23 || chipid[1] != 0x06) {
+		dev_err(dev, "ChipID: 0x%02x 0x%02x\n", chipid[0], chipid[1]);
+		return -ENODEV;
+	}
+
+	return 0;
+}
+
+static ssize_t lt9611c_firmware_store(struct device *dev, struct device_attribute *attr,
+				      const char *buf, size_t len)
+{
+	struct lt9611c *lt9611c = dev_get_drvdata(dev);
+	int ret;
+
+	lt9611c_lock(lt9611c);
+
+	ret = lt9611c_firmware_upgrade(lt9611c);
+	if (ret < 0)
+		dev_err(dev, "upgrade failure\n");
+
+	lt9611c_unlock(lt9611c);
+
+	return ret < 0 ? ret : len;
+}
+
+static ssize_t lt9611c_firmware_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+	struct lt9611c *lt9611c = dev_get_drvdata(dev);
+
+	return sysfs_emit(buf, "0x%04x\n", lt9611c->fw_version);
+}
+
+static DEVICE_ATTR_RW(lt9611c_firmware);
+
+static struct attribute *lt9611c_attrs[] = {
+	&dev_attr_lt9611c_firmware.attr,
+	NULL,
+};
+
+static const struct attribute_group lt9611c_attr_group = {
+	.attrs = lt9611c_attrs,
+};
+
+static const struct attribute_group *lt9611c_attr_groups[] = {
+	&lt9611c_attr_group,
+	NULL,
+};
+
+static int lt9611c_probe(struct i2c_client *client)
+{
+	struct lt9611c *lt9611c;
+	struct device *dev = &client->dev;
+	bool fw_updated = false;
+	int ret;
+
+	crc8_populate_msb(lt9611c_crc8_table, LT9611C_CRC_POLYNOMIAL);
+
+	if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C))
+		return dev_err_probe(dev, -ENODEV, "device doesn't support I2C\n");
+
+	lt9611c = devm_drm_bridge_alloc(dev, struct lt9611c, bridge, &lt9611c_bridge_funcs);
+	if (IS_ERR(lt9611c))
+		return dev_err_probe(dev, PTR_ERR(lt9611c), "drm bridge alloc failed.\n");
+
+	lt9611c->dev = dev;
+	lt9611c->client = client;
+	lt9611c->chip_type = (unsigned long)i2c_get_match_data(client);
+
+	ret = devm_mutex_init(dev, &lt9611c->ocm_lock);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to init mutex\n");
+
+	lt9611c->regmap = devm_regmap_init_i2c(client, &lt9611c_regmap_config);
+	if (IS_ERR(lt9611c->regmap))
+		return dev_err_probe(dev, PTR_ERR(lt9611c->regmap), "regmap i2c init failed\n");
+
+	ret = lt9611c_parse_dt(dev, lt9611c);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to parse device tree\n");
+
+	lt9611c->reset_gpio = devm_gpiod_get(dev, "reset", GPIOD_OUT_HIGH);
+	if (IS_ERR(lt9611c->reset_gpio)) {
+		ret = PTR_ERR(lt9611c->reset_gpio);
+		goto err_of_put;
+	}
+
+	ret = lt9611c_regulator_init(lt9611c);
+	if (ret < 0)
+		goto err_of_put;
+
+	ret = regulator_bulk_enable(ARRAY_SIZE(lt9611c->supplies), lt9611c->supplies);
+	if (ret)
+		goto err_of_put;
+
+	lt9611c_reset(lt9611c);
+
+	lt9611c_lock(lt9611c);
+
+	ret = lt9611c_read_chipid(lt9611c);
+	if (ret < 0) {
+		dev_err(dev, "failed to read chip id.\n");
+		lt9611c_unlock(lt9611c);
+		goto err_disable_regulators;
+	}
+
+retry:
+	lt9611c->fw_version = lt9611c_read_version(lt9611c);
+	if (lt9611c->fw_version < 0) {
+		dev_err(dev, "failed to read fw version\n");
+		ret = -EOPNOTSUPP;
+		lt9611c_unlock(lt9611c);
+		goto err_disable_regulators;
+
+	} else if (lt9611c->fw_version == 0) {
+		if (!fw_updated) {
+			fw_updated = true;
+			ret = lt9611c_firmware_upgrade(lt9611c);
+			if (ret < 0) {
+				lt9611c_unlock(lt9611c);
+				goto err_disable_regulators;
+			}
+
+			goto retry;
+
+		} else {
+			dev_err(dev, "fw version 0x%04x, update failed\n", lt9611c->fw_version);
+			ret = -EOPNOTSUPP;
+			lt9611c_unlock(lt9611c);
+			goto err_disable_regulators;
+		}
+	}
+
+	lt9611c_unlock(lt9611c);
+	dev_dbg(dev, "current version:0x%04x", lt9611c->fw_version);
+
+	INIT_WORK(&lt9611c->work, lt9611c_hpd_work);
+
+	ret = devm_request_threaded_irq(&client->dev, client->irq, NULL,
+					lt9611c_irq_thread_handler,
+					IRQF_TRIGGER_FALLING |
+					IRQF_ONESHOT |
+					IRQF_NO_AUTOEN,
+					"lt9611c", lt9611c);
+	if (ret) {
+		dev_err(dev, "failed to request irq\n");
+		goto err_disable_regulators;
+	}
+
+	lt9611c->bridge.of_node = client->dev.of_node;
+	lt9611c->bridge.ops = DRM_BRIDGE_OP_DETECT |
+			DRM_BRIDGE_OP_EDID |
+			DRM_BRIDGE_OP_HPD |
+			DRM_BRIDGE_OP_HDMI |
+			DRM_BRIDGE_OP_HDMI_AUDIO;
+	lt9611c->bridge.type = DRM_MODE_CONNECTOR_HDMIA;
+
+	lt9611c->bridge.vendor = "Lontium";
+	lt9611c->bridge.product = "LT9611C";
+
+	lt9611c->bridge.hdmi_audio_dev = dev;
+	lt9611c->bridge.hdmi_audio_max_i2s_playback_channels = 8;
+	lt9611c->bridge.hdmi_audio_dai_port = 2;
+
+	devm_drm_bridge_add(dev, &lt9611c->bridge);
+
+	/* Attach primary DSI */
+	lt9611c->dsi0 = lt9611c_attach_dsi(lt9611c, lt9611c->dsi0_node);
+	if (IS_ERR(lt9611c->dsi0)) {
+		ret = PTR_ERR(lt9611c->dsi0);
+		goto err_remove_bridge;
+	}
+
+	/* Attach secondary DSI, if specified */
+	if (lt9611c->dsi1_node) {
+		lt9611c->dsi1 = lt9611c_attach_dsi(lt9611c, lt9611c->dsi1_node);
+		if (IS_ERR(lt9611c->dsi1)) {
+			ret = PTR_ERR(lt9611c->dsi1);
+			goto err_remove_bridge;
+		}
+	}
+
+	lt9611c->hdmi_connected = false;
+	i2c_set_clientdata(client, lt9611c);
+	enable_irq(client->irq);
+
+	return 0;
+
+err_remove_bridge:
+	cancel_work_sync(&lt9611c->work);
+
+err_disable_regulators:
+	regulator_bulk_disable(ARRAY_SIZE(lt9611c->supplies), lt9611c->supplies);
+
+err_of_put:
+	of_node_put(lt9611c->dsi1_node);
+	of_node_put(lt9611c->dsi0_node);
+
+	return ret;
+}
+
+static void lt9611c_remove(struct i2c_client *client)
+{
+	struct lt9611c *lt9611c = i2c_get_clientdata(client);
+
+	disable_irq(client->irq);
+	cancel_work_sync(&lt9611c->work);
+	regulator_bulk_disable(ARRAY_SIZE(lt9611c->supplies), lt9611c->supplies);
+	of_node_put(lt9611c->dsi1_node);
+	of_node_put(lt9611c->dsi0_node);
+}
+
+static int lt9611c_bridge_suspend(struct device *dev)
+{
+	struct lt9611c *lt9611c = dev_get_drvdata(dev);
+	int ret;
+
+	disable_irq(lt9611c->client->irq);
+	cancel_work_sync(&lt9611c->work);
+
+	gpiod_set_value_cansleep(lt9611c->reset_gpio, 1);
+
+	ret = regulator_bulk_disable(ARRAY_SIZE(lt9611c->supplies), lt9611c->supplies);
+	if (ret)
+		dev_err(lt9611c->dev, "regulator bulk disable failed.\n");
+
+	return ret;
+}
+
+static int lt9611c_bridge_resume(struct device *dev)
+{
+	struct lt9611c *lt9611c = dev_get_drvdata(dev);
+	int ret;
+
+	ret = regulator_bulk_enable(ARRAY_SIZE(lt9611c->supplies), lt9611c->supplies);
+	if (ret) {
+		dev_err(lt9611c->dev, "regulator bulk enable failed.\n");
+		return ret;
+	}
+	enable_irq(lt9611c->client->irq);
+	lt9611c_reset(lt9611c);
+
+	return ret;
+}
+
+static const struct dev_pm_ops lt9611c_bridge_pm_ops = {
+	SET_SYSTEM_SLEEP_PM_OPS(lt9611c_bridge_suspend,
+				lt9611c_bridge_resume)
+};
+
+static struct i2c_device_id lt9611c_id[] = {
+	/* chip_type */
+	{ "lontium,lt9611c", CHIP_LT9611C },
+	{ "lontium,lt9611ex", CHIP_LT9611EX },
+	{ "lontium,lt9611uxd", CHIP_LT9611UXD },
+	{ /* sentinel */ }
+};
+
+static const struct of_device_id lt9611c_match_table[] = {
+	{ .compatible = "lontium,lt9611c", .data = (void *)CHIP_LT9611C },
+	{ .compatible = "lontium,lt9611ex", .data = (void *)CHIP_LT9611EX },
+	{ .compatible = "lontium,lt9611uxd", .data = (void *)CHIP_LT9611UXD },
+	{ /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, lt9611c_match_table);
+
+static struct i2c_driver lt9611c_driver = {
+	.driver = {
+		.name = "lt9611c",
+		.of_match_table = lt9611c_match_table,
+		.pm = &lt9611c_bridge_pm_ops,
+		.dev_groups = lt9611c_attr_groups,
+	},
+	.probe = lt9611c_probe,
+	.remove = lt9611c_remove,
+	.id_table = lt9611c_id,
+};
+module_i2c_driver(lt9611c_driver);
+
+MODULE_AUTHOR("SunYun Yang <syyang@lontium.com>");
+MODULE_DESCRIPTION("Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver");
+MODULE_LICENSE("GPL");
+MODULE_FIRMWARE(FW_FILE);

-- 
2.34.1


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

* Re: [PATCH v8 1/2] dt-bindings: bridge: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver
  2026-07-28 11:09 ` [PATCH v8 1/2] dt-bindings: bridge: " mohit.dsor
@ 2026-07-28 11:14   ` sashiko-bot
  0 siblings, 0 replies; 5+ messages in thread
From: sashiko-bot @ 2026-07-28 11:14 UTC (permalink / raw)
  To: mohit.dsor; +Cc: robh, conor+dt, dri-devel, devicetree

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] The schema lacks a conditional constraint to restrict `port@1` for the 1-port LT9611C variant, and the title incorrectly describes all devices as 2-port.
- [Low] Use of Chinese ideographic commas instead of standard ASCII commas in the description.
--

--- Patch [1]: [PATCH v8 1/2] dt-bindings: bridge: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver ---
commit 479e58120e4ea7ccc6ba2a2452966c2bdee319fa
Author: Sunyun Yang <syyang@lontium.com>

dt-bindings: bridge: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver

This commit adds compatible strings and documentation for the Lontium LT9611C,
LT9611EX, and LT9611UXD MIPI DSI to HDMI bridges.

> diff --git a/Documentation/devicetree/bindings/display/bridge/lontium,lt9611.yaml b/Documentation/devicetree/bindings/display/bridge/lontium,lt9611.yaml
> index 429a06057ae85..e0821a63d9d74 100644
> --- a/Documentation/devicetree/bindings/display/bridge/lontium,lt9611.yaml
> +++ b/Documentation/devicetree/bindings/display/bridge/lontium,lt9611.yaml
> @@ -4,19 +4,23 @@
>  $id: http://devicetree.org/schemas/display/bridge/lontium,lt9611.yaml#
>  $schema: http://devicetree.org/meta-schemas/core.yaml#
>  
> -title: Lontium LT9611(UXC) 2 Port MIPI to HDMI Bridge
> +title: Lontium LT9611(UXC/C/EX/UXD) 2 Port MIPI DSI to HDMI Bridge

[Severity: Medium]
The commit message notes that the LT9611C variant supports 1-port MIPI DSI.
Does this title need to be adjusted since it broadly describes all of the
supported devices as "2 Port"?

>  maintainers:
>    - Vinod Koul <vkoul@kernel.org>
>  
>  description: |
> -  The LT9611 and LT9611UXC are bridge devices which convert DSI to HDMI
> +  The LT9611、LT9611UXC、LT9611C、LT9611EX and LT9611UXD

[Severity: Low]
This isn't a bug, but there appear to be non-ascii ideographic commas
used in this list instead of standard commas. Should these be replaced to
prevent rendering or text-processing quirks in the documentation?

> +  are bridge devices which convert DSI to HDMI
>  
>  properties:
>    compatible:
>      enum:
>        - lontium,lt9611
> +      - lontium,lt9611c

[Severity: Medium]
Since the LT9611C variant only supports a single MIPI DSI port, should the
schema include an allOf: conditional block to explicitly set port@1: false
for the lontium,lt9611c compatible string?

Without this constraint, device tree configurations that accidentally include
an invalid second DSI port (port@1) will silently pass schema validation
for this hardware variant.

> +      - lontium,lt9611ex
>        - lontium,lt9611uxc
> +      - lontium,lt9611uxd

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260728-lt9611c-v7-v8-0-bda216bfaf37@oss.qualcomm.com?part=1

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

* Re: [PATCH v8 2/2] drm/bridge: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver
  2026-07-28 11:09 ` [PATCH v8 2/2] drm/bridge: " mohit.dsor
@ 2026-07-28 11:26   ` sashiko-bot
  0 siblings, 0 replies; 5+ messages in thread
From: sashiko-bot @ 2026-07-28 11:26 UTC (permalink / raw)
  To: mohit.dsor; +Cc: conor+dt, dri-devel, devicetree, robh

Thank you for your contribution! Sashiko AI review found 12 potential issue(s) to consider:
- [Critical] Recursive locking deadlock in `lt9611c_hdmi_audio_prepare`.
- [High] The IRQ is enabled before the hardware reset is completed during resume.
- [High] Premature manual teardown in remove() conflicts with devm_* APIs, leading to UAF and timeouts.
- [High] The IRQ handler incorrectly clears unhandled interrupts by writing an inverted bitmask.
- [High] Early registration of the DRM bridge exposes it before resource initialization is complete, risking use-after-free on probe failure.
- [Medium] The return value of devm_drm_bridge_add() is ignored.
- [Medium] Selecting REGMAP_I2C without a dependency on I2C causes a Kconfig warning and build failure.
- [Medium] request_firmware() is called while holding the ocm_lock mutex.
- [Medium] Missing `regmap_read()` error handling causes uninitialized memory usage and broken firmware upgrade logic.
- [Medium] Incomplete error recovery in suspend callback leaves the device in a permanently broken state.
- [Medium] Synchronous firmware request during probe may permanently fail if the root filesystem is not yet mounted.
- [Low] Firmware version is not updated in the driver state after a successful sysfs upgrade.
--

--- Patch [2]: [PATCH v8 2/2] drm/bridge: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver ---
commit d9ec2b705d140567b617d8df2b85d294039b2f5c
Author: Sunyun Yang <syyang@lontium.com>

drm/bridge: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver

This commit adds a DRM bridge driver for the Lontium LT9611C,
LT9611EX, and LT9611UXD DSI to HDMI chips. It supports single or
dual MIPI DSI and I2S/SPDIF audio input to HDMI output. The driver
handles firmware loading, hot plug detection, and video/audio
configuration.

> diff --git a/drivers/gpu/drm/bridge/Kconfig b/drivers/gpu/drm/bridge/Kconfig
> --- a/drivers/gpu/drm/bridge/Kconfig
> +++ b/drivers/gpu/drm/bridge/Kconfig
> @@ -177,6 +177,23 @@ config DRM_LONTIUM_LT9611
[ ... ]
> +config DRM_LONTIUM_LT9611C
> +	tristate "Lontium LT9611C DSI/HDMI bridge"
> +	select SND_SOC_HDMI_CODEC if SND_SOC
> +	depends on OF
> +	select CRC8
> +	select FW_LOADER
> +	select DRM_PANEL_BRIDGE
> +	select DRM_KMS_HELPER
> +	select DRM_MIPI_DSI
> +	select DRM_DISPLAY_HELPER
> +	select DRM_DISPLAY_HDMI_STATE_HELPER
> +	select REGMAP_I2C

[Severity: Medium]
Is there a missing dependency on I2C here?

Since this config selects REGMAP_I2C, if I2C is disabled, this could
violate Kconfig dependency rules and trigger an unmet direct dependencies
warning, causing build failures for the I2C subsystem functions.

> diff --git a/drivers/gpu/drm/bridge/lontium-lt9611c.c b/drivers/gpu/drm/bridge/lontium-lt9611c.c
> --- /dev/null
> +++ b/drivers/gpu/drm/bridge/lontium-lt9611c.c
[ ... ]
> +static void read_flash_reg_status(struct lt9611c *lt9611c, unsigned int *status)
> +{
[ ... ]
> +	regmap_multi_reg_write(lt9611c->regmap, seq_write, ARRAY_SIZE(seq_write));
> +
> +	regmap_read(lt9611c->regmap, 0xe05f, status);
> +}

[Severity: Medium]
What happens if this regmap_read() fails?

If the I2C read drops a packet, the status variable will retain its
previous value (which is 0 in the caller's initialization).

This would cause the erase loop condition in lt9611c_block_erase():
    if ((flash_status & 0x01) == 0)
to evaluate to true immediately, potentially breaking the firmware
erase and upgrade logic.

[ ... ]
> +static int lt9611c_upgrade_result(struct lt9611c *lt9611c, u8 fw_crc)
> +{
> +	struct device *dev = lt9611c->dev;
> +	unsigned int crc_result;
> +
> +	regmap_write(lt9611c->regmap, 0xe0ee, 0x01);
> +	regmap_read(lt9611c->regmap, 0xe021, &crc_result);
> +
> +	if (crc_result != fw_crc) {

[Severity: Medium]
Should the return value of regmap_read() be checked here?

If the I2C read drops a packet, crc_result will contain uninitialized
memory when it is compared against fw_crc.

This could lead to a false success or failure of the upgrade check.

[ ... ]
> +static int lt9611c_firmware_upgrade(struct lt9611c *lt9611c)
> +{
> +	struct device *dev = lt9611c->dev;
> +	const struct firmware *fw;
[ ... ]
> +	/* 1. load firmware */
> +	ret = request_firmware(&fw, FW_FILE, dev);
> +	if (ret)
> +		return dev_err_probe(dev, ret, "failed to load '%s'\n", FW_FILE);

[Severity: Medium]
Will this cause permanent probe failures if the root filesystem is delayed?

When fw_version is 0, this calls request_firmware() synchronously from
lt9611c_probe(). If the firmware is not built-in and rootfs isn't
mounted yet, it fails with -ENOENT.

By propagating this error through dev_err_probe() rather than returning
-EPROBE_DEFER, the driver will fail permanently.

[Severity: Medium]
Is it safe to call request_firmware() while holding the ocm_lock?

This function is also called synchronously from lt9611c_firmware_store()
with the hardware-protecting ocm_lock held.

If the firmware file is missing, the firmware helper can block for up to
60 seconds. Holding the lock during this time will stall the device's
IRQ handler and any DRM atomic operations that need it.

[ ... ]
> +static irqreturn_t lt9611c_irq_thread_handler(int irq, void *dev_id)
> +{
[ ... ]
> +	if (!(irq_status & BIT(0)))
> +		return IRQ_HANDLED;
> +
> +	/*Clear interrupt: hardware requires two writes with delay*/
> +	regmap_write(lt9611c->regmap, 0xe0df, irq_status & BIT(0));
> +	usleep_range(10000, 12000);
> +	regmap_write(lt9611c->regmap, 0xe0df, irq_status & (~BIT(0)));

[Severity: High]
Could this clear sequence suppress other pending interrupts?

This writes the inverted bitmask to the clear register. If irq_status
indicates other interrupts are pending (such as bit 1), this will write
a 1 for bit 1.

Since no corresponding 0 is ever written to complete the pulse for bit 1,
its clear signal might be asserted indefinitely. This could either
improperly acknowledge it or permanently suppress it.

[ ... ]
> +static int lt9611c_hdmi_write_audio_infoframe(struct drm_bridge *bridge,
> +					      const u8 *buffer, size_t len)
> +{
[ ... ]
> +	guard(mutex)(&lt9611c->ocm_lock);
> +
> +	return lt9611c_read_write_flow(lt9611c, &cmd, &rsp);
> +}

[ ... ]
> +static int lt9611c_hdmi_audio_prepare(struct drm_bridge *bridge,
> +				      struct drm_connector *connector,
> +				      struct hdmi_codec_daifmt *fmt,
> +				      struct hdmi_codec_params *hparms)
> +{
[ ... ]
> +	guard(mutex)(&lt9611c->ocm_lock);
> +
> +	ret = lt9611c_read_write_flow(lt9611c, &cmd, &rsp);
> +	if (ret < 0) {
> +		dev_err(lt9611c->dev, "set audio info failed!\n");
> +		return ret;
> +	}
> +
> +	return drm_atomic_helper_connector_hdmi_update_audio_infoframe(connector,
> +									&hparms->cea);
> +}

[Severity: Critical]
Does this sequence result in a recursive locking deadlock?

In lt9611c_hdmi_audio_prepare(), guard(mutex)(&lt9611c->ocm_lock)
is acquired. While holding this lock, it calls
drm_atomic_helper_connector_hdmi_update_audio_infoframe().

That helper packs the infoframe and calls the connector's write_infoframe
hook, which routes back to lt9611c_hdmi_write_audio_infoframe().

This callback also attempts to acquire lt9611c->ocm_lock using
guard(mutex), leading to a self-deadlock since mutexes are not recursive.

[ ... ]
> +static ssize_t lt9611c_firmware_store(struct device *dev, struct device_attribute *attr,
> +				      const char *buf, size_t len)
> +{
> +	struct lt9611c *lt9611c = dev_get_drvdata(dev);
> +	int ret;
> +
> +	lt9611c_lock(lt9611c);
> +
> +	ret = lt9611c_firmware_upgrade(lt9611c);
> +	if (ret < 0)
> +		dev_err(dev, "upgrade failure\n");
> +
> +	lt9611c_unlock(lt9611c);
> +
> +	return ret < 0 ? ret : len;
> +}

[Severity: Low]
Should the cached firmware version be updated here?

If the firmware upgrade via sysfs succeeds, lt9611c->fw_version is
never updated.

Subsequent reads of the sysfs file will display the old version until
the device is rebound or the system is rebooted.

[ ... ]
> +static int lt9611c_probe(struct i2c_client *client)
> +{
[ ... ]
> +	lt9611c->bridge.hdmi_audio_max_i2s_playback_channels = 8;
> +	lt9611c->bridge.hdmi_audio_dai_port = 2;
> +
> +	devm_drm_bridge_add(dev, &lt9611c->bridge);

[Severity: Medium]
Should the return value of devm_drm_bridge_add() be checked?

If the devres allocation fails, it will return an error, but the
driver ignores it and continues probing. This could lead to a
silent display pipeline failure.

[Severity: High]
Could this early bridge registration cause a use-after-free?

The bridge is registered globally here before the primary and
secondary MIPI DSI interfaces are attached.

> +
> +	/* Attach primary DSI */
> +	lt9611c->dsi0 = lt9611c_attach_dsi(lt9611c, lt9611c->dsi0_node);
> +	if (IS_ERR(lt9611c->dsi0)) {
> +		ret = PTR_ERR(lt9611c->dsi0);
> +		goto err_remove_bridge;
> +	}

If lt9611c_attach_dsi() fails, the probe function errors out. However,
the bridge relies on devres to unregister it.

During this window, another display controller could discover and attach
to the visible bridge, retaining a pointer to the structure that devres
will soon free.

[ ... ]
> +static void lt9611c_remove(struct i2c_client *client)
> +{
> +	struct lt9611c *lt9611c = i2c_get_clientdata(client);
> +
> +	disable_irq(client->irq);
> +	cancel_work_sync(&lt9611c->work);
> +	regulator_bulk_disable(ARRAY_SIZE(lt9611c->supplies), lt9611c->supplies);
> +	of_node_put(lt9611c->dsi1_node);
> +	of_node_put(lt9611c->dsi0_node);
> +}

[Severity: High]
Does this manual teardown conflict with the devm-managed bridge?

The bridge is registered with devm_drm_bridge_add(), so it is only
unregistered by devres after remove() returns. However, this function
manually calls cancel_work_sync() and regulator_bulk_disable().

If the DRM core calls .hpd_enable between remove() returning and
devres cleanup, the driver will call schedule_work() on the cancelled
workqueue.

This could cause a use-after-free when devres finally cleans up the
bridge context.

[ ... ]
> +static int lt9611c_bridge_suspend(struct device *dev)
> +{
[ ... ]
> +	gpiod_set_value_cansleep(lt9611c->reset_gpio, 1);
> +
> +	ret = regulator_bulk_disable(ARRAY_SIZE(lt9611c->supplies), lt9611c->supplies);
> +	if (ret)
> +		dev_err(lt9611c->dev, "regulator bulk disable failed.\n");
> +
> +	return ret;
> +}

[Severity: Medium]
What happens if regulator_bulk_disable() fails here?

If it fails, the function returns an error without reversing the
preceding state changes, such as disabling the IRQ and asserting the
reset GPIO.

The PM core will abort the suspend but won't call the resume callback
for this device, leaving it completely unresponsive.

[ ... ]
> +static int lt9611c_bridge_resume(struct device *dev)
> +{
[ ... ]
> +	ret = regulator_bulk_enable(ARRAY_SIZE(lt9611c->supplies), lt9611c->supplies);
> +	if (ret) {
> +		dev_err(lt9611c->dev, "regulator bulk enable failed.\n");
> +		return ret;
> +	}
> +	enable_irq(lt9611c->client->irq);
> +	lt9611c_reset(lt9611c);
> +
> +	return ret;
> +}

[Severity: High]
Should the IRQ be enabled after the hardware reset is complete?

This calls enable_irq() immediately before lt9611c_reset(). During
the 400+ ms reset sequence, the chip is held in reset and I2C is
unresponsive.

If an interrupt triggers (e.g. from power-up glitches or toggling the
reset pin), the handler will fire and try to read the status register
via I2C while the hardware is still booting, leading to bus timeouts
or lockups.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260728-lt9611c-v7-v8-0-bda216bfaf37@oss.qualcomm.com?part=2

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

end of thread, other threads:[~2026-07-28 11:26 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-28 11:09 [PATCH v8 0/2] Subject: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver mohit.dsor
2026-07-28 11:09 ` [PATCH v8 1/2] dt-bindings: bridge: " mohit.dsor
2026-07-28 11:14   ` sashiko-bot
2026-07-28 11:09 ` [PATCH v8 2/2] drm/bridge: " mohit.dsor
2026-07-28 11:26   ` sashiko-bot

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.