* [PATCH v9 0/2] Subject: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver
@ 2026-08-04 12:40 mohit.dsor
2026-08-04 12:40 ` [PATCH v9 1/2] dt-bindings: bridge: " mohit.dsor
2026-08-04 12:40 ` [PATCH v9 2/2] drm/bridge: " mohit.dsor
0 siblings, 2 replies; 7+ messages in thread
From: mohit.dsor @ 2026-08-04 12:40 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
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 v9:
- Use regmap_bulk_write for 4-byte header write in read_write_flow
- Replace while(1) loop with read_poll_timeout in lt9611c_block_erase; read_flash_reg_status now returns unsigned int
- Fix unchecked regmap_read in lt9611c_upgrade_result; return -EIO on CRC mismatch
- Add struct lt9611c_chip_data with chip_type and max_tmds_rate; fix i2c_device_id table driver_data to use chip data pointers
- Use put_unaligned_be16() to replace manual high/low byte splits in lt9611c_video_setup
- Extract lt9611c_write_infoframe() and lt9611c_clear_infoframe() helpers; consolidate 6 infoframe functions into 2 helpers
- Remove lt9611c_hdmi_audio_startup and its bridge_funcs entry
- Remove hpd_enable callback from bridge_funcs
- Fix scoped_guard(mutex) deadlock in audio_prepare: release lock before calling drm_atomic_helper_connector_hdmi_update_audio_infoframe()
- Fix probe order: lt9611c_reset() before enable_irq()
- Fix remove(): remove disable_irq() (IRQ is devm_request_threaded_irq-managed)
- Fix bridge_suspend() unwind: on regulator_bulk_disable failure, de-assert reset and re-enable IRQ
- Move devm_drm_bridge_add to after both DSI attaches; remove cancel_work_sync from err_remove_bridge label
- Move request_firmware outside mcu_lock in lt9611c_firmware_upgrade; lock only wraps hardware access
- Re-read lt9611c->fw_version after successful firmware_store upgrade
- Rename ocm_lock to mcu_lock throughout
- Use goto err_put_dsi1 to eliminate duplicate DSI cleanup in probe error path
- Fix mcu_lock comment to correctly describe what it protects
- dt-binding: add if:then: block to disallow port@1 when compatible is lontium,lt9611c
- Rename sysfs ABI doc to sysfs-driver-lontium; rename sysfs attribute from lt9611c_firmware to firmware
- Kconfig: add I2C to depends on to satisfy REGMAP_I2C dependency
- Link to v8: https://lore.kernel.org/lkml/20260728-lt9611c-v7-v8-0-bda216bfaf37@oss.qualcomm.com/
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
Documentation/ABI/testing/sysfs-lontium-firmware | 10 +
.../bindings/display/bridge/lontium,lt9611.yaml | 18 +-
drivers/gpu/drm/bridge/Kconfig | 18 +
drivers/gpu/drm/bridge/Makefile | 1 +
drivers/gpu/drm/bridge/lontium-lt9611c.c | 1263 ++++++++++++++++++++
5 files changed, 1308 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] 7+ messages in thread
* [PATCH v9 1/2] dt-bindings: bridge: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver
2026-08-04 12:40 [PATCH v9 0/2] Subject: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver mohit.dsor
@ 2026-08-04 12:40 ` mohit.dsor
2026-08-06 7:12 ` Krzysztof Kozlowski
2026-08-04 12:40 ` [PATCH v9 2/2] drm/bridge: " mohit.dsor
1 sibling, 1 reply; 7+ messages in thread
From: mohit.dsor @ 2026-08-04 12:40 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>
Signed-off-by: Mohit Dsor <mohit.dsor@oss.qualcomm.com>
---
.../bindings/display/bridge/lontium,lt9611.yaml | 18 ++++++++++++++++--
1 file changed, 16 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..194aeaac601f 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
@@ -78,6 +82,16 @@ required:
allOf:
- $ref: /schemas/sound/dai-common.yaml#
+ - if:
+ properties:
+ compatible:
+ contains:
+ const: lontium,lt9611c
+ then:
+ properties:
+ ports:
+ properties:
+ port@1: false
unevaluatedProperties: false
--
2.34.1
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH v9 2/2] drm/bridge: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver
2026-08-04 12:40 [PATCH v9 0/2] Subject: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver mohit.dsor
2026-08-04 12:40 ` [PATCH v9 1/2] dt-bindings: bridge: " mohit.dsor
@ 2026-08-04 12:40 ` mohit.dsor
2026-08-04 12:59 ` sashiko-bot
1 sibling, 1 reply; 7+ messages in thread
From: mohit.dsor @ 2026-08-04 12:40 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>
---
Documentation/ABI/testing/sysfs-lontium-firmware | 10 +
drivers/gpu/drm/bridge/Kconfig | 18 +
drivers/gpu/drm/bridge/Makefile | 1 +
drivers/gpu/drm/bridge/lontium-lt9611c.c | 1263 ++++++++++++++++++++++
4 files changed, 1292 insertions(+)
diff --git a/Documentation/ABI/testing/sysfs-lontium-firmware b/Documentation/ABI/testing/sysfs-lontium-firmware
new file mode 100644
index 000000000000..8700933ba0f0
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-lontium-firmware
@@ -0,0 +1,10 @@
+What: /sys/bus/i2c/drivers/<driver>/.../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
+ Lontium bridge chip in hex format (e.g. "0x0105").
+
+ Write: Triggers a firmware upgrade of the chip using the
+ firmware file loaded via the firmware loader.
diff --git a/drivers/gpu/drm/bridge/Kconfig b/drivers/gpu/drm/bridge/Kconfig
index 4a57d49b4c6d..d1597548e578 100644
--- a/drivers/gpu/drm/bridge/Kconfig
+++ b/drivers/gpu/drm/bridge/Kconfig
@@ -177,6 +177,24 @@ 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 && I2C
+ 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. Supports HDMI 1.4 (LT9611C/EX) and HDMI 2.0 (LT9611UXD).
+ 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..f5af2856ce00
--- /dev/null
+++ b/drivers/gpu/drm/bridge/lontium-lt9611c.c
@@ -0,0 +1,1263 @@
+// 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 <linux/unaligned.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_chip_data {
+ enum lt9611_chip_type chip_type;
+ unsigned long long max_tmds_rate;
+};
+
+static const struct lt9611c_chip_data lt9611c_chip_data[] = {
+ [CHIP_LT9611C] = { CHIP_LT9611C, 340000000 },
+ [CHIP_LT9611EX] = { CHIP_LT9611EX, 340000000 },
+ [CHIP_LT9611UXD] = { CHIP_LT9611UXD, 600000000 },
+};
+
+struct lt9611c {
+ struct device *dev;
+ struct i2c_client *client;
+ struct drm_bridge bridge;
+ struct regmap *regmap;
+ struct mutex mcu_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;
+ unsigned long long max_tmds_rate;
+ 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;
+
+ ret = regmap_bulk_write(lt9611c->regmap, 0xe0b0, &cmd->hdr,
+ LT9611C_CMD_HDR_SIZE);
+ if (ret)
+ return ret;
+
+ 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_lock(struct lt9611c *lt9611c)
+{
+ mutex_lock(<9611c->mcu_lock);
+ regmap_write(lt9611c->regmap, 0xe0ee, 0x01);
+}
+
+static void lt9611c_unlock(struct lt9611c *lt9611c)
+{
+ regmap_write(lt9611c->regmap, 0xe0ee, 0x00);
+ mutex_unlock(<9611c->mcu_lock);
+}
+
+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 unsigned int read_flash_reg_status(struct lt9611c *lt9611c)
+{
+ 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),
+ };
+ unsigned int status;
+ int ret;
+
+ regmap_multi_reg_write(lt9611c->regmap, seq_write, ARRAY_SIZE(seq_write));
+ ret = regmap_read(lt9611c->regmap, 0xe05f, &status);
+ if (ret)
+ return 0xff;
+
+ return 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)
+{
+ 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);
+ read_poll_timeout(read_flash_reg_status, flash_status,
+ !(flash_status & 0x01),
+ 50 * USEC_PER_MSEC, 2500 * USEC_PER_MSEC,
+ false, lt9611c);
+ }
+}
+
+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 = DIV_ROUND_UP(size, 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;
+ int ret;
+
+ regmap_write(lt9611c->regmap, 0xe0ee, 0x01);
+ ret = regmap_read(lt9611c->regmap, 0xe021, &crc_result);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to read firmware crc\n");
+
+ 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 -EIO;
+ }
+
+ 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;
+
+ /* load firmware — must happen outside the mcu_lock */
+ ret = request_firmware(&fw, FW_FILE, dev);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to load '%s'\n", FW_FILE);
+
+ 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);
+
+ 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);
+
+ /* hardware access requires mcu_lock */
+ lt9611c_lock(lt9611c);
+
+ 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_unlock;
+ }
+
+ ret = lt9611c_write_crc(lt9611c, fw_crc, FW_SIZE - 1);
+ if (ret < 0) {
+ dev_err(dev, "failed to write firmware crc\n");
+ goto out_unlock;
+ }
+
+ lt9611c_reset(lt9611c);
+ ret = lt9611c_upgrade_result(lt9611c, fw_crc);
+
+out_unlock:
+ lt9611c_unlock(lt9611c);
+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 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)(<9611c->mcu_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(<9611c->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(<9611c->mcu_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(<9611c->mcu_lock);
+
+ drm_bridge_hpd_notify(<9611c->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 (tmds_rate > lt9611c->max_tmds_rate)
+ 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)(<9611c->mcu_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);
+
+ put_unaligned_be16(h_total, &timing_data[0]);
+ put_unaligned_be16(hactive, &timing_data[2]);
+ put_unaligned_be16(hfront_porch, &timing_data[4]);
+ put_unaligned_be16(hsync_len, &timing_data[6]);
+ put_unaligned_be16(hback_porch, &timing_data[8]);
+ put_unaligned_be16(v_total, &timing_data[10]);
+ put_unaligned_be16(vactive, &timing_data[12]);
+ put_unaligned_be16(vfront_porch, &timing_data[14]);
+ put_unaligned_be16(vsync_len, &timing_data[16]);
+ put_unaligned_be16(vback_porch, &timing_data[18]);
+ 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)(<9611c->mcu_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)(<9611c->mcu_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_write_infoframe(struct lt9611c *lt9611c, u8 type,
+ const u8 *buffer, size_t len)
+{
+ 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] = type;
+ memcpy(&extra[1], buffer, len);
+ cmd.data = extra;
+ cmd.data_len = 1 + len;
+
+ guard(mutex)(<9611c->mcu_lock);
+
+ return lt9611c_read_write_flow(lt9611c, &cmd, &rsp);
+}
+
+static int lt9611c_clear_infoframe(struct lt9611c *lt9611c, u8 type)
+{
+ u8 clear_data = type;
+ 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 = {};
+
+ guard(mutex)(<9611c->mcu_lock);
+
+ return lt9611c_read_write_flow(lt9611c, &cmd, &rsp);
+}
+
+static int lt9611c_hdmi_write_avi_infoframe(struct drm_bridge *bridge,
+ const u8 *buffer, size_t len)
+{
+ return lt9611c_write_infoframe(bridge_to_lt9611c(bridge), 0x01, buffer, len);
+}
+
+static int lt9611c_hdmi_clear_avi_infoframe(struct drm_bridge *bridge)
+{
+ return lt9611c_clear_infoframe(bridge_to_lt9611c(bridge), 0x01);
+}
+
+static int lt9611c_hdmi_write_hdmi_infoframe(struct drm_bridge *bridge,
+ const u8 *buffer, size_t len)
+{
+ return lt9611c_write_infoframe(bridge_to_lt9611c(bridge), 0x04, buffer, len);
+}
+
+static int lt9611c_hdmi_clear_hdmi_infoframe(struct drm_bridge *bridge)
+{
+ return lt9611c_clear_infoframe(bridge_to_lt9611c(bridge), 0x04);
+}
+
+static int lt9611c_hdmi_write_audio_infoframe(struct drm_bridge *bridge,
+ const u8 *buffer, size_t len)
+{
+ return lt9611c_write_infoframe(bridge_to_lt9611c(bridge), 0x02, buffer, len);
+}
+
+static int lt9611c_hdmi_clear_audio_infoframe(struct drm_bridge *bridge)
+{
+ return lt9611c_clear_infoframe(bridge_to_lt9611c(bridge), 0x02);
+}
+
+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;
+
+ scoped_guard(mutex, <9611c->mcu_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 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,
+
+ .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_prepare = lt9611c_hdmi_audio_prepare,
+ .hdmi_audio_shutdown = lt9611c_hdmi_audio_shutdown,
+};
+
+static int lt9611c_parse_dt(struct device *dev,
+ struct lt9611c *lt9611c)
+{
+ int ret;
+
+ 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) {
+ ret = dev_err_probe(dev, -EINVAL,
+ "LT9611C does not support dual DSI\n");
+ goto err_put_dsi1;
+ }
+
+ lt9611c->bridge.next_bridge = of_drm_get_bridge_by_endpoint(dev->of_node, 2, -1);
+ if (IS_ERR(lt9611c->bridge.next_bridge)) {
+ ret = PTR_ERR(lt9611c->bridge.next_bridge);
+ goto err_put_dsi1;
+ }
+
+ return 0;
+
+err_put_dsi1:
+ of_node_put(lt9611c->dsi1_node);
+ of_node_put(lt9611c->dsi0_node);
+ return ret;
+}
+
+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 firmware_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t len)
+{
+ struct lt9611c *lt9611c = dev_get_drvdata(dev);
+ int ret;
+
+ ret = lt9611c_firmware_upgrade(lt9611c);
+ if (ret < 0) {
+ dev_err(dev, "upgrade failure\n");
+ return ret;
+ }
+
+ lt9611c_lock(lt9611c);
+ lt9611c->fw_version = lt9611c_read_version(lt9611c);
+ lt9611c_unlock(lt9611c);
+
+ if (lt9611c->fw_version < 0)
+ dev_warn(dev, "upgrade succeeded but failed to read new fw version\n");
+ else
+ dev_info(dev, "firmware upgrade succeeded, version: 0x%04x\n",
+ lt9611c->fw_version);
+
+ return len;
+}
+
+static ssize_t 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(firmware);
+
+static struct attribute *lt9611c_attrs[] = {
+ &dev_attr_firmware.attr,
+ NULL,
+};
+
+static const struct attribute_group lt9611c_attr_group = {
+ .attrs = lt9611c_attrs,
+};
+
+static const struct attribute_group *lt9611c_attr_groups[] = {
+ <9611c_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, <9611c_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;
+ const struct lt9611c_chip_data *cdata = i2c_get_match_data(client);
+
+ lt9611c->chip_type = cdata->chip_type;
+ lt9611c->max_tmds_rate = cdata->max_tmds_rate;
+
+ ret = devm_mutex_init(dev, <9611c->mcu_lock);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to init mutex\n");
+
+ lt9611c->regmap = devm_regmap_init_i2c(client, <9611c_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;
+ lt9611c_unlock(lt9611c);
+ ret = lt9611c_firmware_upgrade(lt9611c);
+ if (ret < 0)
+ goto err_disable_regulators;
+ lt9611c_lock(lt9611c);
+ 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(<9611c->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;
+
+ /* Attach primary DSI */
+ lt9611c->dsi0 = lt9611c_attach_dsi(lt9611c, lt9611c->dsi0_node);
+ if (IS_ERR(lt9611c->dsi0)) {
+ ret = PTR_ERR(lt9611c->dsi0);
+ goto err_disable_regulators;
+ }
+
+ /* 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_disable_regulators;
+ }
+ }
+
+ lt9611c->hdmi_connected = false;
+ i2c_set_clientdata(client, lt9611c);
+
+ devm_drm_bridge_add(dev, <9611c->bridge);
+
+ enable_irq(client->irq);
+
+ return 0;
+
+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);
+
+ cancel_work_sync(<9611c->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(<9611c->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");
+ gpiod_set_value_cansleep(lt9611c->reset_gpio, 0);
+ enable_irq(lt9611c->client->irq);
+ return ret;
+ }
+
+ return 0;
+}
+
+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;
+ }
+ lt9611c_reset(lt9611c);
+ enable_irq(lt9611c->client->irq);
+
+ 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[] = {
+ { "lontium,lt9611c", (kernel_ulong_t)<9611c_chip_data[CHIP_LT9611C] },
+ { "lontium,lt9611ex", (kernel_ulong_t)<9611c_chip_data[CHIP_LT9611EX] },
+ { "lontium,lt9611uxd", (kernel_ulong_t)<9611c_chip_data[CHIP_LT9611UXD] },
+ { /* sentinel */ }
+};
+
+static const struct of_device_id lt9611c_match_table[] = {
+ { .compatible = "lontium,lt9611c", .data = <9611c_chip_data[CHIP_LT9611C] },
+ { .compatible = "lontium,lt9611ex", .data = <9611c_chip_data[CHIP_LT9611EX] },
+ { .compatible = "lontium,lt9611uxd", .data = <9611c_chip_data[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 = <9611c_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] 7+ messages in thread
* Re: [PATCH v9 2/2] drm/bridge: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver
2026-08-04 12:40 ` [PATCH v9 2/2] drm/bridge: " mohit.dsor
@ 2026-08-04 12:59 ` sashiko-bot
2026-08-07 19:47 ` Mohit Dsor
0 siblings, 1 reply; 7+ messages in thread
From: sashiko-bot @ 2026-08-04 12:59 UTC (permalink / raw)
To: mohit.dsor; +Cc: robh, dri-devel, devicetree, conor+dt
Thank you for your contribution! Sashiko AI review found 8 potential issue(s) to consider:
- [High] Missing `disable_irq()` before `cancel_work_sync()` in `lt9611c_remove()` causes a use-after-free when `devm` frees the device context.
- [High] Bridge is left registered and accessible to DRM core while its power regulators are disabled during module unload.
- [High] Unconditional dereference of `cdata` returned by `i2c_get_match_data()` without checking for NULL.
- [Medium] Incorrect interrupt clearing sequence in `lt9611c_irq_thread_handler()` can permanently suppress other pending interrupts.
- [Medium] The return value of `devm_drm_bridge_add()` is ignored, which can lead to silent probe failures.
- [Medium] Firmware upgrade via sysfs silently resets the bridge hardware, breaking active DRM display state.
- [Medium] The IRQ handler incorrectly returns `IRQ_HANDLED` when the interrupt was not generated by this device.
- [Medium] The return value of `read_poll_timeout` is ignored during flash block erase, which can lead to firmware corruption.
--
--- Patch [2]: [PATCH v9 2/2] drm/bridge: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver ---
commit 6e88557e672677022a4675c645ef3cbef1ee7ce0
Author: Sunyun Yang <syyang@lontium.com>
Subject: drm/bridge: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver
This commit introduces a DRM bridge driver for the Lontium LT9611C,
LT9611EX, and LT9611UXD DSI-to-HDMI chips. It handles configuration over I2C,
video timing setup, audio infoframe handling, and firmware upgrades.
> diff --git a/drivers/gpu/drm/bridge/lontium-lt9611c.c b/drivers/gpu/drm/bridge/lontium-lt9611c.c
> new file mode 100644
> index 0000000000000..f5af2856ce00b
> --- /dev/null
> +++ b/drivers/gpu/drm/bridge/lontium-lt9611c.c
[ ... ]
> +static void lt9611c_block_erase(struct lt9611c *lt9611c)
> +{
> + 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);
> + read_poll_timeout(read_flash_reg_status, flash_status,
> + !(flash_status & 0x01),
> + 50 * USEC_PER_MSEC, 2500 * USEC_PER_MSEC,
> + false, lt9611c);
[Severity: Medium]
Is it safe to ignore the return value of read_poll_timeout() here?
If the hardware fails to erase the flash within the timeout, the driver
will proceed to lt9611c_write_data() anyway. This would cause it to write
new firmware data over un-erased or partially erased flash pages,
potentially corrupting the device firmware permanently.
[ ... ]
> +static int lt9611c_firmware_upgrade(struct lt9611c *lt9611c)
> +{
[ ... ]
> + ret = lt9611c_write_crc(lt9611c, fw_crc, FW_SIZE - 1);
> + if (ret < 0) {
> + dev_err(dev, "failed to write firmware crc\n");
> + goto out_unlock;
> + }
> +
> + lt9611c_reset(lt9611c);
> + ret = lt9611c_upgrade_result(lt9611c, fw_crc);
[Severity: Medium]
Could this reset break the active DRM display state?
When userspace writes to the firmware sysfs attribute, this function
resets the bridge chip without notifying the DRM subsystem. This wipes all
video timings and configurations, meaning an active display pipeline will
go blank and become desynchronized from the DRM state.
[ ... ]
> +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)(<9611c->mcu_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;
[Severity: Medium]
Should this early return use IRQ_NONE instead?
Returning IRQ_HANDLED when the interrupt does not belong to this device
defeats the kernel's spurious interrupt detection mechanism. If the IRQ
line becomes noisy or stuck, the kernel will falsely assume this driver
handled it and will not disable the line, potentially causing an interrupt
storm that freezes the system.
> +
> + /*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: Medium]
Could this interrupt clearing sequence accidentally suppress other pending
interrupts?
If multiple interrupts were pending (for example, bit 1 is also set in
irq_status), the second write outputs a 1 for those other bits because of
the bitwise inversion logic. If the hardware clears interrupts via a
pulse mechanism, those other bits are left stuck at 1 and are never written
back to 0. This would break the clearing mechanism for any interrupt
other than the primary bit.
[ ... ]
> +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, <9611c_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;
> + const struct lt9611c_chip_data *cdata = i2c_get_match_data(client);
> +
> + lt9611c->chip_type = cdata->chip_type;
[Severity: High]
Can cdata be NULL here?
When the device is instantiated via the sysfs bind attribute or new_device
without a valid match table entry (e.g., matching by driver name fallback),
i2c_get_match_data() will return NULL. Dereferencing cdata immediately
would result in a NULL pointer dereference and a kernel panic.
[ ... ]
> + devm_drm_bridge_add(dev, <9611c->bridge);
> +
> + enable_irq(client->irq);
> +
> + return 0;
[Severity: Medium]
Is there a missing error check for devm_drm_bridge_add()?
Since devm_drm_bridge_add() can return an error code like -ENOMEM,
ignoring it allows the driver to probe successfully without registering the
bridge. This leaves a dysfunctional driver bound to the device, silently
consuming resources.
[ ... ]
> +static void lt9611c_remove(struct i2c_client *client)
> +{
> + struct lt9611c *lt9611c = i2c_get_clientdata(client);
> +
> + cancel_work_sync(<9611c->work);
[Severity: High]
Could this lead to a use-after-free since the IRQ is not disabled before
cancelling the work?
The IRQ was requested with devm_request_threaded_irq, meaning it remains
active until devres cleanup, which happens after lt9611c_remove() returns.
An interrupt firing right after cancel_work_sync() completes will invoke
the threaded handler, which calls schedule_work(<9611c->work). When
devres frees the lt9611c structure, the newly queued work isn't cancelled,
causing the workqueue to eventually execute lt9611c_hpd_work on freed memory.
> + regulator_bulk_disable(ARRAY_SIZE(lt9611c->supplies), lt9611c->supplies);
[Severity: High]
Does this prematurely power down the chip while the bridge is still active?
Because the bridge was registered with devm_drm_bridge_add(), the DRM core
will only unregister it during devres teardown, which runs after this
remove function completes. By manually disabling the regulators here, there
is a window where the bridge is globally visible and can receive DRM calls
(such as a detect call from userspace polling), but the hardware is powered
off. This can cause I2C timeouts or bus lockups during module unload.
> + of_node_put(lt9611c->dsi1_node);
> + of_node_put(lt9611c->dsi0_node);
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260804-lt9611c-v7-v9-0-3423a76856d3@oss.qualcomm.com?part=2
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH v9 1/2] dt-bindings: bridge: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver
2026-08-04 12:40 ` [PATCH v9 1/2] dt-bindings: bridge: " mohit.dsor
@ 2026-08-06 7:12 ` Krzysztof Kozlowski
2026-08-07 10:05 ` Mohit Dsor
0 siblings, 1 reply; 7+ messages in thread
From: Krzysztof Kozlowski @ 2026-08-06 7:12 UTC (permalink / raw)
To: mohit.dsor
Cc: 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, dri-devel, devicetree, linux-kernel, venkata.valluru,
Jessica Zhang
On Tue, Aug 04, 2026 at 06:10:14PM +0530, mohit.dsor@oss.qualcomm.com wrote:
> 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 <mohit.dsor@oss.qualcomm.com>
> ---
> .../bindings/display/bridge/lontium,lt9611.yaml | 18 ++++++++++++++++--
> 1 file changed, 16 insertions(+), 2 deletions(-)
Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski@oss.qualcomm.com>
Best regards,
Krzysztof
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH v9 1/2] dt-bindings: bridge: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver
2026-08-06 7:12 ` Krzysztof Kozlowski
@ 2026-08-07 10:05 ` Mohit Dsor
0 siblings, 0 replies; 7+ messages in thread
From: Mohit Dsor @ 2026-08-07 10:05 UTC (permalink / raw)
To: Krzysztof Kozlowski
Cc: 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, dri-devel, devicetree, linux-kernel, venkata.valluru,
Jessica Zhang
On Thu, Aug 06, 2026 at 09:12:59AM +0200, Krzysztof Kozlowski wrote:
> On Tue, Aug 04, 2026 at 06:10:14PM +0530, mohit.dsor@oss.qualcomm.com wrote:
> > 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 <mohit.dsor@oss.qualcomm.com>
> > ---
> > .../bindings/display/bridge/lontium,lt9611.yaml | 18 ++++++++++++++++--
> > 1 file changed, 16 insertions(+), 2 deletions(-)
>
> Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski@oss.qualcomm.com>
Thanks, Krzysztof.
>
> Best regards,
> Krzysztof
>
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH v9 2/2] drm/bridge: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver
2026-08-04 12:59 ` sashiko-bot
@ 2026-08-07 19:47 ` Mohit Dsor
0 siblings, 0 replies; 7+ messages in thread
From: Mohit Dsor @ 2026-08-07 19:47 UTC (permalink / raw)
To: sashiko-reviews; +Cc: robh, dri-devel, devicetree, conor+dt
On Tue, Aug 04, 2026 at 12:59:38PM +0000, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 8 potential issue(s) to consider:
> - [High] Missing `disable_irq()` before `cancel_work_sync()` in `lt9611c_remove()` causes a use-after-free when `devm` frees the device context.
> - [High] Bridge is left registered and accessible to DRM core while its power regulators are disabled during module unload.
> - [High] Unconditional dereference of `cdata` returned by `i2c_get_match_data()` without checking for NULL.
> - [Medium] Incorrect interrupt clearing sequence in `lt9611c_irq_thread_handler()` can permanently suppress other pending interrupts.
> - [Medium] The return value of `devm_drm_bridge_add()` is ignored, which can lead to silent probe failures.
> - [Medium] Firmware upgrade via sysfs silently resets the bridge hardware, breaking active DRM display state.
> - [Medium] The IRQ handler incorrectly returns `IRQ_HANDLED` when the interrupt was not generated by this device.
> - [Medium] The return value of `read_poll_timeout` is ignored during flash block erase, which can lead to firmware corruption.
> --
>
> --- Patch [2]: [PATCH v9 2/2] drm/bridge: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver ---
> commit 6e88557e672677022a4675c645ef3cbef1ee7ce0
> Author: Sunyun Yang <syyang@lontium.com>
> Subject: drm/bridge: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver
>
> This commit introduces a DRM bridge driver for the Lontium LT9611C,
> LT9611EX, and LT9611UXD DSI-to-HDMI chips. It handles configuration over I2C,
> video timing setup, audio infoframe handling, and firmware upgrades.
>
> > diff --git a/drivers/gpu/drm/bridge/lontium-lt9611c.c b/drivers/gpu/drm/bridge/lontium-lt9611c.c
> > new file mode 100644
> > index 0000000000000..f5af2856ce00b
> > --- /dev/null
> > +++ b/drivers/gpu/drm/bridge/lontium-lt9611c.c
> [ ... ]
> > +static void lt9611c_block_erase(struct lt9611c *lt9611c)
> > +{
> > + 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);
> > + read_poll_timeout(read_flash_reg_status, flash_status,
> > + !(flash_status & 0x01),
> > + 50 * USEC_PER_MSEC, 2500 * USEC_PER_MSEC,
> > + false, lt9611c);
>
> [Severity: Medium]
> Is it safe to ignore the return value of read_poll_timeout() here?
Added the check here.
>
> If the hardware fails to erase the flash within the timeout, the driver
> will proceed to lt9611c_write_data() anyway. This would cause it to write
> new firmware data over un-erased or partially erased flash pages,
> potentially corrupting the device firmware permanently.
>
> [ ... ]
> > +static int lt9611c_firmware_upgrade(struct lt9611c *lt9611c)
> > +{
> [ ... ]
> > + ret = lt9611c_write_crc(lt9611c, fw_crc, FW_SIZE - 1);
> > + if (ret < 0) {
> > + dev_err(dev, "failed to write firmware crc\n");
> > + goto out_unlock;
> > + }
> > +
> > + lt9611c_reset(lt9611c);
> > + ret = lt9611c_upgrade_result(lt9611c, fw_crc);
>
> [Severity: Medium]
> Could this reset break the active DRM display state?
>
> When userspace writes to the firmware sysfs attribute, this function
> resets the bridge chip without notifying the DRM subsystem. This wipes all
> video timings and configurations, meaning an active display pipeline will
> go blank and become desynchronized from the DRM state.
No, it is needed. Added warn lof for this.
>
> [ ... ]
> > +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)(<9611c->mcu_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;
>
> [Severity: Medium]
> Should this early return use IRQ_NONE instead?
Yes, fixed this in v10.
>
> Returning IRQ_HANDLED when the interrupt does not belong to this device
> defeats the kernel's spurious interrupt detection mechanism. If the IRQ
> line becomes noisy or stuck, the kernel will falsely assume this driver
> handled it and will not disable the line, potentially causing an interrupt
> storm that freezes the system.
>
> > +
> > + /*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: Medium]
> Could this interrupt clearing sequence accidentally suppress other pending
> interrupts?
As shared earlier only 1 interrupt is there.
>
> If multiple interrupts were pending (for example, bit 1 is also set in
> irq_status), the second write outputs a 1 for those other bits because of
> the bitwise inversion logic. If the hardware clears interrupts via a
> pulse mechanism, those other bits are left stuck at 1 and are never written
> back to 0. This would break the clearing mechanism for any interrupt
> other than the primary bit.
>
> [ ... ]
> > +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, <9611c_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;
> > + const struct lt9611c_chip_data *cdata = i2c_get_match_data(client);
> > +
> > + lt9611c->chip_type = cdata->chip_type;
>
> [Severity: High]
> Can cdata be NULL here?
Yes check is added in v10.
>
> When the device is instantiated via the sysfs bind attribute or new_device
> without a valid match table entry (e.g., matching by driver name fallback),
> i2c_get_match_data() will return NULL. Dereferencing cdata immediately
> would result in a NULL pointer dereference and a kernel panic.
>
> [ ... ]
> > + devm_drm_bridge_add(dev, <9611c->bridge);
> > +
> > + enable_irq(client->irq);
> > +
> > + return 0;
>
> [Severity: Medium]
> Is there a missing error check for devm_drm_bridge_add()?
Added the check.
>
> Since devm_drm_bridge_add() can return an error code like -ENOMEM,
> ignoring it allows the driver to probe successfully without registering the
> bridge. This leaves a dysfunctional driver bound to the device, silently
> consuming resources.
>
> [ ... ]
> > +static void lt9611c_remove(struct i2c_client *client)
> > +{
> > + struct lt9611c *lt9611c = i2c_get_clientdata(client);
> > +
> > + cancel_work_sync(<9611c->work);
>
> [Severity: High]
> Could this lead to a use-after-free since the IRQ is not disabled before
> cancelling the work?
Fixed this in v10.
>
> The IRQ was requested with devm_request_threaded_irq, meaning it remains
> active until devres cleanup, which happens after lt9611c_remove() returns.
> An interrupt firing right after cancel_work_sync() completes will invoke
> the threaded handler, which calls schedule_work(<9611c->work). When
> devres frees the lt9611c structure, the newly queued work isn't cancelled,
> causing the workqueue to eventually execute lt9611c_hpd_work on freed memory.
>
> > + regulator_bulk_disable(ARRAY_SIZE(lt9611c->supplies), lt9611c->supplies);
>
> [Severity: High]
> Does this prematurely power down the chip while the bridge is still active?
Fixed this in v10.
>
> Because the bridge was registered with devm_drm_bridge_add(), the DRM core
> will only unregister it during devres teardown, which runs after this
> remove function completes. By manually disabling the regulators here, there
> is a window where the bridge is globally visible and can receive DRM calls
> (such as a detect call from userspace polling), but the hardware is powered
> off. This can cause I2C timeouts or bus lockups during module unload.
>
> > + of_node_put(lt9611c->dsi1_node);
> > + of_node_put(lt9611c->dsi0_node);
> > +}
>
> --
> Sashiko AI review · https://sashiko.dev/#/patchset/20260804-lt9611c-v7-v9-0-3423a76856d3@oss.qualcomm.com?part=2
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-08-07 19:48 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-04 12:40 [PATCH v9 0/2] Subject: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver mohit.dsor
2026-08-04 12:40 ` [PATCH v9 1/2] dt-bindings: bridge: " mohit.dsor
2026-08-06 7:12 ` Krzysztof Kozlowski
2026-08-07 10:05 ` Mohit Dsor
2026-08-04 12:40 ` [PATCH v9 2/2] drm/bridge: " mohit.dsor
2026-08-04 12:59 ` sashiko-bot
2026-08-07 19:47 ` Mohit Dsor
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox