* [PATCH v18 0/2] Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver
@ 2026-10-21 7:57 mohit.dsor
2026-10-21 7:57 ` [PATCH v18 1/2] dt-bindings: bridge: " mohit.dsor
2026-10-21 7:57 ` [PATCH v18 2/2] drm/bridge: " mohit.dsor
0 siblings, 2 replies; 5+ messages in thread
From: mohit.dsor @ 2026-10-21 7:57 UTC (permalink / raw)
To: 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: Mohit Dsor, dri-devel, devicetree, linux-kernel, venkata.valluru,
Jessica Zhang, Sunyun Yang, Krzysztof Kozlowski
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 v18:
- Fix commit message: add hardware feature list and driver capabilities
description that was missing.
- Rename lt9611c_wren/lt9611c_wrdi to lt9611c_wr_enable/lt9611c_wr_disable
for clarity.
- Replace heap allocation for CRC with incremental computation over
fw->data followed by padding bytes, avoiding a full FW_SIZE buffer.
- Use total_size instead of FW_SIZE - 1 in lt9611c_write_crc call.
- Make total_size const.
- Improve HPD delay comment: explain 200 ms is empirically required for
MCU debounce after interrupt.
- Use ARRAY_SIZE() instead of hardcoded 2 in devm_regulator_bulk_get.
- Move guard(mutex) in lt9611c_video_setup to just before the MCU command
instead of before timing struct setup.
- Add NULL check in firmware_store and firmware_show to guard against
access before i2c_set_clientdata() is called in probe.
- Add inline comments for MCU register addresses 0xe085.
- Fix double blank line in lt9611c_read_write_flow.
- Link to v17: https://lore.kernel.org/lkml/20261008-lt9611c-v7-v17-0-1e8f5f2b2f19@oss.qualcomm.com/
Changes in v17:
- Add dual DSI port enable: introduce lt9611c_dual_port_enable() which
sends the MCU command to enable dual DSI input mode on LT9611UXD/EX
chips. Called from lt9611c_bridge_atomic_enable() after video_setup
when two DSI hosts are connected. Add bool dual_dsi to struct lt9611c,
set at parse_dt when secondary DSI port is found.
- Link to v16: https://lore.kernel.org/lkml/20260916-lt9611c-v7-v16-0-c65042fcd245@oss.qualcomm.com/
Changes in v16:
- Fix interrupt clear sequence: write BIT(0) to assert clear then 0x00
to deassert, instead of writing irq_status & ~BIT(0) which could
inadvertently toggle other pending interrupt bits
- Fix suspend error recovery path: add msleep(400) before re-enabling
IRQ after reset GPIO deassert, matching the delay in lt9611c_reset()
to allow MCU to initialize before interrupts are enabled
- Link to v15: https://lore.kernel.org/all/20260915-lt9611c-v7-v15-0-88f9e9387eab@oss.qualcomm.com/
Changes in v15:
- Fix firmware flash corruption: poll for flash WIP (write-in-progress)
bit to clear after each lt9611c_sram_to_flash() call in both
lt9611c_write_data() and lt9611c_write_crc(), preventing SRAM from
being overwritten while the SPI flash controller is still programming
- Fix missing infoframe flush during modeset: call
drm_atomic_helper_connector_hdmi_update_infoframes() at the end of
lt9611c_bridge_atomic_enable() to ensure AVI and other infoframes are
transmitted after video setup
- Fix data race on fw_version: annotate concurrent read in
firmware_show() with READ_ONCE and write in firmware_store() with
WRITE_ONCE
- Link to v14: https://lore.kernel.org/dri-devel/20260913-lt9611c-v7-v14-0-534b43a07662@oss.qualcomm.com/
Changes in v14:
- Fix device node leak: redirect GPIO and regulator init failure paths
to err_put_nodes label instead of returning directly, ensuring
dsi0_node and dsi1_node are always released via of_node_put()
- Link to v13: https://lore.kernel.org/dri-devel/20260909-lt9611c-v7-v13-0-aec234483725@oss.qualcomm.com/
Changes in v13:
- Fix double-free of dsi0_node/dsi1_node: remove __free(device_node) which
is prohibited when mixed with goto; use explicit of_node_put in all exit
paths
- Fix const and named initializers for i2c_device_id table
- Link to v12: https://lore.kernel.org/lkml/20260903-lt9611c-v7-v12-0-9414d2b3bd6d@oss.qualcomm.com/
Changes in v12:
- Fix audio deadlock in lt9611c_hdmi_audio_prepare: release mcu_lock before
calling drm_atomic_helper_connector_hdmi_update_audio_infoframe which
re-enters the driver via lt9611c_hdmi_write_audio_infoframe
- Move lock acquisition from lt9611c_write_infoframe/lt9611c_clear_infoframe
helpers to individual bridge callbacks; add lockdep_assert_held in helpers
- Remove unused includes: platform_device.h, drm_drv.h, drm_print.h, drm_of.h,
unaligned.h
- Remove DRM_PANEL_BRIDGE from Kconfig (unused)
- Fix copyright: (c) -> (C) for Qualcomm line
- Remove dsi0_node, dsi1_node, dsi0, dsi1 from struct lt9611c; make them
local to probe using __free(device_node) and local dsi pointer
- Replace chip_type/max_tmds_rate fields with single cdata pointer in struct
- Fix sysfs NULL dereference race: move i2c_set_clientdata to top of probe
- Fix concurrent crc8_populate_msb: move to module_init
- Fix i2c_device_id: remove vendor prefix (was a workaround for NULL driver_data)
- Replace timing_data byte array with named struct using __be16 fields
- Rename page to npages in lt9611c_write_data
- Move loop-local variables inside loop in lt9611c_write_data
- Add DRM_BRIDGE_ATTACH_NO_CONNECTOR check in bridge_attach
- Use sizeof(timing_data) instead of ARRAY_SIZE
- Move regulator_bulk_enable inside lt9611c_regulator_init
- Various nits: comment spacing, blank lines, variable initialization
- Link to v11: https://lore.kernel.org/dri-devel/20260824-lt9611c-v7-v11-0-eb4a79cb597c@oss.qualcomm.com/
Changes in v11:
- Fix firmware path: use "Lontium/lt9611c_fw.bin" to match linux-firmware layout
- Link to v10: https://lore.kernel.org/dri-devel/20260808-lt9611c-v7-v10-0-ee90a136d82a@oss.qualcomm.com/
Changes in v10:
- Fix lt9611c_block_erase: check return value of read_poll_timeout to avoid writing to un-erased flash
- Fix NULL dereference: check i2c_get_match_data() return value before dereferencing
- Fix irq handler: return IRQ_NONE when BIT(0) is not set
- Fix devm_drm_bridge_add: check and propagate error return
- Fix use-after-free in remove: call disable_irq() before cancel_work_sync()
- Fix bridge/regulator ordering in remove: use drm_bridge_add/drm_bridge_remove explicitly so bridge is unregistered before regulators are disabled
- Promote firmware upgrade success log from dev_dbg to dev_info
- Link to v9: https://lore.kernel.org/lkml/20260804-lt9611c-v7-v9-0-3423a76856d3@oss.qualcomm.com/
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>
---
To: Andrzej Hajda <andrzej.hajda@intel.com>
To: Neil Armstrong <neil.armstrong@linaro.org>
To: Robert Foss <rfoss@kernel.org>
To: Laurent Pinchart <Laurent.pinchart@ideasonboard.com>
To: Jonas Karlman <jonas@kwiboo.se>
To: Jernej Skrabec <jernej.skrabec@gmail.com>
To: Luca Ceresoli <luca.ceresoli@bootlin.com>
To: Maarten Lankhorst <maarten.lankhorst@linux.intel.com>
To: Maxime Ripard <mripard@kernel.org>
To: Thomas Zimmermann <tzimmermann@suse.de>
To: David Airlie <airlied@gmail.com>
To: Simona Vetter <simona@ffwll.ch>
To: Rob Herring <robh@kernel.org>
To: Krzysztof Kozlowski <krzk+dt@kernel.org>
To: Conor Dooley <conor+dt@kernel.org>
To: Vinod Koul <vkoul@kernel.org>
Cc: dri-devel@lists.freedesktop.org
Cc: devicetree@vger.kernel.org
Cc: linux-kernel@vger.kernel.org
---
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 | 17 +
drivers/gpu/drm/bridge/Makefile | 1 +
drivers/gpu/drm/bridge/lontium-lt9611c.c | 1361 ++++++++++++++++++++
5 files changed, 1405 insertions(+), 2 deletions(-)
---
base-commit: ebdae09876325cb00da572d82ded6c7253c409fa
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 v18 1/2] dt-bindings: bridge: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver
2026-10-21 7:57 [PATCH v18 0/2] Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver mohit.dsor
@ 2026-10-21 7:57 ` mohit.dsor
2026-10-21 7:57 ` [PATCH v18 2/2] drm/bridge: " mohit.dsor
1 sibling, 0 replies; 5+ messages in thread
From: mohit.dsor @ 2026-10-21 7:57 UTC (permalink / raw)
To: 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: Mohit Dsor, dri-devel, devicetree, linux-kernel, venkata.valluru,
Jessica Zhang, Sunyun Yang, Krzysztof Kozlowski
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
Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski@oss.qualcomm.com>
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] 5+ messages in thread
* [PATCH v18 2/2] drm/bridge: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver
2026-10-21 7:57 [PATCH v18 0/2] Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver mohit.dsor
2026-10-21 7:57 ` [PATCH v18 1/2] dt-bindings: bridge: " mohit.dsor
@ 2026-10-21 7:57 ` mohit.dsor
2026-10-21 8:23 ` sashiko-bot
2026-10-27 17:57 ` Luca Ceresoli
1 sibling, 2 replies; 5+ messages in thread
From: mohit.dsor @ 2026-10-21 7:57 UTC (permalink / raw)
To: 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: Mohit Dsor, dri-devel, devicetree, linux-kernel, venkata.valluru,
Jessica Zhang, Sunyun Yang
From: Sunyun Yang <syyang@lontium.com>
LT9611C(EX/UXD) is an I2C-controlled chip that receives single or dual
port MIPI DSI and outputs 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
The driver supports:
- EDID read and mode filtering via custom MCU command protocol
- HPD detection and interrupt handling
- HDMI audio output via I2S
- Firmware upgrade via sysfs
- Dual DSI bonded mode for LT9611EX and LT9611UXD variants
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 | 17 +
drivers/gpu/drm/bridge/Makefile | 1 +
drivers/gpu/drm/bridge/lontium-lt9611c.c | 1361 ++++++++++++++++++++++
4 files changed, 1389 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 c7da6b02d7b6..0a66831fb482 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 && I2C
+ select CRC8
+ select FW_LOADER
+ 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..8fc86c073212
--- /dev/null
+++ b/drivers/gpu/drm/bridge/lontium-lt9611c.c
@@ -0,0 +1,1361 @@
+// 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/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_edid.h>
+#include <drm/drm_mipi_dsi.h>
+#include <drm/drm_modes.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 gpio_desc *reset_gpio;
+ struct regulator_bulk_data supplies[2];
+ int fw_version;
+ const struct lt9611c_chip_data *cdata;
+ bool hdmi_connected;
+ bool dual_dsi;
+};
+
+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, /* MCU response buffer */
+ &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, /* data follows header */
+ 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_wr_enable(struct lt9611c *lt9611c)
+{
+ regmap_write(lt9611c->regmap, 0xe05a, 0x04);
+ regmap_write(lt9611c->regmap, 0xe05a, 0x00);
+}
+
+static void lt9611c_wr_disable(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 int lt9611c_block_erase(struct lt9611c *lt9611c)
+{
+ struct device *dev = lt9611c->dev;
+ unsigned int block_num;
+ unsigned int flash_status = 0;
+ u32 flash_addr = 0;
+ int ret;
+
+ for (block_num = 0; block_num < 2; block_num++) {
+ flash_addr = block_num * 0x008000;
+ lt9611c_erase_op(lt9611c, flash_addr);
+ msleep(100);
+ ret = read_poll_timeout(read_flash_reg_status, flash_status,
+ !(flash_status & 0x01),
+ 50 * USEC_PER_MSEC, 2500 * USEC_PER_MSEC,
+ false, lt9611c);
+ if (ret) {
+ dev_err(dev, "flash erase timeout for block %u\n", block_num);
+ return ret;
+ }
+ }
+
+ return 0;
+}
+
+static int lt9611c_write_data(struct lt9611c *lt9611c, const struct firmware *fw, size_t addr)
+{
+ struct device *dev = lt9611c->dev;
+ unsigned int npages;
+ unsigned int flash_status;
+ int ret;
+ size_t size = fw->size;
+ const u8 *data = fw->data;
+
+ npages = DIV_ROUND_UP(size, LT_PAGE_SIZE);
+ if (npages * 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", npages, size);
+
+ for (unsigned int num = 0; num < npages; num++) {
+ lt9611c_data_to_sram(lt9611c);
+
+ for (unsigned int i = 0; i < LT_PAGE_SIZE; i++) {
+ size_t index = num * LT_PAGE_SIZE + i;
+ u8 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_wr_enable(lt9611c);
+ lt9611c_sram_to_flash(lt9611c, addr);
+
+ ret = read_poll_timeout(read_flash_reg_status, flash_status,
+ !(flash_status & 0x01),
+ 50 * USEC_PER_MSEC, 2500 * USEC_PER_MSEC,
+ false, lt9611c);
+ if (ret) {
+ dev_err(dev, "flash write timeout at page %u\n", num);
+ return ret;
+ }
+
+ addr += LT_PAGE_SIZE;
+ }
+
+ lt9611c_wr_disable(lt9611c);
+
+ return 0;
+}
+
+static int lt9611c_write_crc(struct lt9611c *lt9611c, u8 fw_crc, size_t addr)
+{
+ struct device *dev = lt9611c->dev;
+ unsigned int flash_status;
+ 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_wr_enable(lt9611c);
+ lt9611c_sram_to_flash(lt9611c, addr);
+
+ ret = read_poll_timeout(read_flash_reg_status, flash_status,
+ !(flash_status & 0x01),
+ 50 * USEC_PER_MSEC, 2500 * USEC_PER_MSEC,
+ false, lt9611c);
+ if (ret) {
+ dev_err(dev, "flash crc write timeout\n");
+ return ret;
+ }
+
+ lt9611c_wr_disable(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_info(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;
+ const size_t total_size = FW_SIZE - 1;
+ u8 fw_crc;
+ int ret;
+
+ 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);
+
+ fw_crc = crc8(lt9611c_crc8_table, fw->data, fw->size, 0);
+ for (unsigned int i = 0; i < total_size - fw->size; i++)
+ fw_crc = lt9611c_crc8_table[(fw_crc ^ 0xff) & 0xff];
+
+ 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);
+ ret = lt9611c_block_erase(lt9611c);
+ if (ret < 0)
+ goto out_unlock;
+
+ 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, total_size);
+ 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_NONE;
+
+ /* Clear interrupt: hardware requires two writes with delay */
+ regmap_write(lt9611c->regmap, 0xe0df, BIT(0));
+ usleep_range(10000, 12000);
+ regmap_write(lt9611c->regmap, 0xe0df, 0x00);
+
+ 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;
+
+ /*
+ * Wait for the MCU to debounce and latch the HPD state after the
+ * interrupt fires. The MCU needs time to update its internal HPD
+ * register; reading immediately after the interrupt returns stale
+ * state. 200 ms is empirically sufficient.
+ */
+ 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, ARRAY_SIZE(lt9611c->supplies), lt9611c->supplies);
+ if (ret)
+ return ret;
+
+ return regulator_bulk_enable(ARRAY_SIZE(lt9611c->supplies), lt9611c->supplies);
+}
+
+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);
+
+ if (!(flags & DRM_BRIDGE_ATTACH_NO_CONNECTOR))
+ return -EINVAL;
+
+ 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->cdata->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;
+ struct {
+ __be16 htotal;
+ __be16 hactive;
+ __be16 hfront_porch;
+ __be16 hsync_len;
+ __be16 hback_porch;
+ __be16 vtotal;
+ __be16 vactive;
+ __be16 vfront_porch;
+ __be16 vsync_len;
+ __be16 vback_porch;
+ u8 framerate;
+ u8 vic;
+ } timing_data;
+ struct lt9611c_rsp rsp = {};
+ struct lt9611c_cmd cmd = {
+ .hdr = { LT9611C_FUNC_WRITE, LT9611C_TYPE_MIPI, 0x33, LT9611C_CMD_SEP },
+ .data = (u8 *)&timing_data,
+ .data_len = sizeof(timing_data),
+ };
+
+ timing_data.htotal = cpu_to_be16(mode->htotal);
+ timing_data.hactive = cpu_to_be16(mode->hdisplay);
+ timing_data.hfront_porch = cpu_to_be16(mode->hsync_start - mode->hdisplay);
+ timing_data.hsync_len = cpu_to_be16(mode->hsync_end - mode->hsync_start);
+ timing_data.hback_porch = cpu_to_be16(mode->htotal - mode->hsync_end);
+ timing_data.vtotal = cpu_to_be16(mode->vtotal);
+ timing_data.vactive = cpu_to_be16(mode->vdisplay);
+ timing_data.vfront_porch = cpu_to_be16(mode->vsync_start - mode->vdisplay);
+ timing_data.vsync_len = cpu_to_be16(mode->vsync_end - mode->vsync_start);
+ timing_data.vback_porch = cpu_to_be16(mode->vtotal - mode->vsync_end);
+ timing_data.framerate = drm_mode_vrefresh(mode);
+ timing_data.vic = drm_match_cea_mode(mode);
+
+ dev_dbg(dev, "hactive=%d, vactive=%d\n", mode->hdisplay, mode->vdisplay);
+ dev_dbg(dev, "framerate=%d\n", timing_data.framerate);
+ dev_dbg(dev, "vic = 0x%02x\n", timing_data.vic);
+
+ guard(mutex)(<9611c->mcu_lock);
+
+ ret = lt9611c_read_write_flow(lt9611c, &cmd, &rsp);
+ if (ret)
+ dev_err(dev, "video set failed\n");
+}
+
+/*
+ * Enables dual DSI port input on the LT9611UXD/EX. Must be called
+ * after video_setup when two DSI hosts are connected.
+ */
+static int lt9611c_dual_port_enable(struct lt9611c *lt9611c)
+{
+ static const u8 dual_port_data[] = { 0x02, 0x70 };
+ struct device *dev = lt9611c->dev;
+ u8 ack = 0;
+ int ret;
+ struct lt9611c_rsp rsp = { .data = &ack, .data_len = 1 };
+ struct lt9611c_cmd cmd = {
+ .hdr = { LT9611C_FUNC_WRITE, LT9611C_TYPE_MIPI, 0x31, LT9611C_CMD_SEP },
+ .data = dual_port_data,
+ .data_len = ARRAY_SIZE(dual_port_data),
+ };
+
+ guard(mutex)(<9611c->mcu_lock);
+
+ ret = lt9611c_read_write_flow(lt9611c, &cmd, &rsp);
+ if (ret || !ack) {
+ dev_err(dev, "failed to enable dual DSI port (err=%d)\n", ret);
+ return ret ? ret : -EIO;
+ }
+
+ return 0;
+}
+
+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;
+ int ret;
+
+ 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);
+
+ if (lt9611c->dual_dsi) {
+ ret = lt9611c_dual_port_enable(lt9611c);
+ if (ret)
+ dev_err(lt9611c->dev, "failed to enable dual DSI port (err=%d)\n", ret);
+ }
+
+ drm_atomic_helper_connector_hdmi_update_infoframes(connector, state);
+}
+
+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;
+
+ lockdep_assert_held(<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 = {};
+
+ lockdep_assert_held(<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)
+{
+ struct lt9611c *lt9611c = bridge_to_lt9611c(bridge);
+
+ guard(mutex)(<9611c->mcu_lock);
+ return lt9611c_write_infoframe(lt9611c, 0x01, buffer, len);
+}
+
+static int lt9611c_hdmi_clear_avi_infoframe(struct drm_bridge *bridge)
+{
+ struct lt9611c *lt9611c = bridge_to_lt9611c(bridge);
+
+ guard(mutex)(<9611c->mcu_lock);
+ return lt9611c_clear_infoframe(lt9611c, 0x01);
+}
+
+static int lt9611c_hdmi_write_hdmi_infoframe(struct drm_bridge *bridge,
+ const u8 *buffer, size_t len)
+{
+ struct lt9611c *lt9611c = bridge_to_lt9611c(bridge);
+
+ guard(mutex)(<9611c->mcu_lock);
+ return lt9611c_write_infoframe(lt9611c, 0x04, buffer, len);
+}
+
+static int lt9611c_hdmi_clear_hdmi_infoframe(struct drm_bridge *bridge)
+{
+ struct lt9611c *lt9611c = bridge_to_lt9611c(bridge);
+
+ guard(mutex)(<9611c->mcu_lock);
+ return lt9611c_clear_infoframe(lt9611c, 0x04);
+}
+
+static int lt9611c_hdmi_write_audio_infoframe(struct drm_bridge *bridge,
+ const u8 *buffer, size_t len)
+{
+ struct lt9611c *lt9611c = bridge_to_lt9611c(bridge);
+
+ guard(mutex)(<9611c->mcu_lock);
+ return lt9611c_write_infoframe(lt9611c, 0x02, buffer, len);
+}
+
+static int lt9611c_hdmi_clear_audio_infoframe(struct drm_bridge *bridge)
+{
+ struct lt9611c *lt9611c = bridge_to_lt9611c(bridge);
+
+ guard(mutex)(<9611c->mcu_lock);
+ return lt9611c_clear_infoframe(lt9611c, 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;
+
+ mutex_lock(<9611c->mcu_lock);
+ ret = lt9611c_read_write_flow(lt9611c, &cmd, &rsp);
+ mutex_unlock(<9611c->mcu_lock);
+ 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,
+ struct device_node **dsi0_node,
+ struct device_node **dsi1_node)
+{
+ int ret;
+
+ *dsi0_node = of_graph_get_remote_node(dev->of_node, 0, -1);
+ if (!*dsi0_node)
+ return dev_err_probe(dev, -ENODEV, "failed to get remote node for primary dsi\n");
+
+ *dsi1_node = of_graph_get_remote_node(dev->of_node, 1, -1);
+
+ if (*dsi1_node && lt9611c->cdata->chip_type == CHIP_LT9611C) {
+ ret = dev_err_probe(dev, -EINVAL,
+ "LT9611C does not support dual DSI\n");
+ goto err_put_dsi1;
+ }
+
+ if (*dsi1_node)
+ lt9611c->dual_dsi = true;
+
+ 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(*dsi1_node);
+ of_node_put(*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;
+
+ if (!lt9611c)
+ return -ENODEV;
+
+ dev_warn(dev, "starting firmware upgrade — display will be disrupted\n");
+
+ ret = lt9611c_firmware_upgrade(lt9611c);
+ if (ret < 0) {
+ dev_err(dev, "upgrade failure\n");
+ return ret;
+ }
+
+ lt9611c_lock(lt9611c);
+ WRITE_ONCE(lt9611c->fw_version, lt9611c_read_version(lt9611c));
+ lt9611c_unlock(lt9611c);
+
+ if (READ_ONCE(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",
+ READ_ONCE(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);
+
+ if (!lt9611c)
+ return -ENODEV;
+
+ return sysfs_emit(buf, "0x%04x\n", READ_ONCE(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;
+ struct device_node *dsi0_node = NULL;
+ struct device_node *dsi1_node = NULL;
+ struct mipi_dsi_device *dsi;
+ bool fw_updated = false;
+ int ret;
+
+ 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;
+ i2c_set_clientdata(client, lt9611c);
+
+ const struct lt9611c_chip_data *cdata = i2c_get_match_data(client);
+
+ if (!cdata)
+ return dev_err_probe(dev, -EINVAL, "no match data for device\n");
+
+ lt9611c->cdata = cdata;
+
+ 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, &dsi0_node, &dsi1_node);
+ 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_put_nodes;
+ }
+
+ ret = lt9611c_regulator_init(lt9611c);
+ if (ret < 0)
+ goto err_put_nodes;
+
+ 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;
+
+ drm_bridge_add(<9611c->bridge);
+
+ /* Attach primary DSI */
+ dsi = lt9611c_attach_dsi(lt9611c, dsi0_node);
+ if (IS_ERR(dsi)) {
+ ret = PTR_ERR(dsi);
+ goto err_remove_bridge;
+ }
+
+ /* Attach secondary DSI, if specified */
+ if (dsi1_node) {
+ dsi = lt9611c_attach_dsi(lt9611c, dsi1_node);
+ if (IS_ERR(dsi)) {
+ ret = PTR_ERR(dsi);
+ goto err_remove_bridge;
+ }
+ }
+
+ lt9611c->hdmi_connected = false;
+ enable_irq(client->irq);
+
+ of_node_put(dsi1_node);
+ of_node_put(dsi0_node);
+
+ return 0;
+
+err_remove_bridge:
+ drm_bridge_remove(<9611c->bridge);
+ cancel_work_sync(<9611c->work);
+
+err_disable_regulators:
+ regulator_bulk_disable(ARRAY_SIZE(lt9611c->supplies), lt9611c->supplies);
+
+err_put_nodes:
+ of_node_put(dsi1_node);
+ of_node_put(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(<9611c->work);
+ drm_bridge_remove(<9611c->bridge);
+ regulator_bulk_disable(ARRAY_SIZE(lt9611c->supplies), lt9611c->supplies);
+}
+
+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);
+ msleep(400);
+ 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 const struct i2c_device_id lt9611c_id[] = {
+ { .name = "lt9611c", .driver_data = (kernel_ulong_t)<9611c_chip_data[CHIP_LT9611C] },
+ { .name = "lt9611ex", .driver_data = (kernel_ulong_t)<9611c_chip_data[CHIP_LT9611EX] },
+ { .name = "lt9611uxd", .driver_data = (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,
+};
+
+static int __init lt9611c_init(void)
+{
+ crc8_populate_msb(lt9611c_crc8_table, LT9611C_CRC_POLYNOMIAL);
+ return i2c_add_driver(<9611c_driver);
+}
+module_init(lt9611c_init);
+
+static void __exit lt9611c_exit(void)
+{
+ i2c_del_driver(<9611c_driver);
+}
+module_exit(lt9611c_exit);
+
+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 v18 2/2] drm/bridge: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver
2026-10-21 7:57 ` [PATCH v18 2/2] drm/bridge: " mohit.dsor
@ 2026-10-21 8:23 ` sashiko-bot
2026-10-27 17:57 ` Luca Ceresoli
1 sibling, 0 replies; 5+ messages in thread
From: sashiko-bot @ 2026-10-21 8:23 UTC (permalink / raw)
To: mohit.dsor; +Cc: dri-devel, devicetree, robh, conor+dt
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [Medium] drm/bridge: lt9611c: negative errno stored in fw_version corrupts firmware_show()
- [Medium] drm/bridge: lt9611c: unlocked reset and power control in PM callbacks
- [Low] drm/bridge: lt9611c: unused function warnings when CONFIG_PM_SLEEP=n
- [Low] drm/bridge: lt9611c: missing MODULE_DEVICE_TABLE() for lt9611c_id
- [Low] drm/bridge: lt9611c: unlocked reset of hdmi_connected after bridge attach
--
commit 990744b0d700fbd77a4343ac0bdea653a817a6ce
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 MIPI DSI to HDMI bridge chips. It supports EDID reading, HPD
detection, HDMI audio over I2S, dual-DSI mode, and firmware upgrades via
sysfs.
> diff --git a/drivers/gpu/drm/bridge/lontium-lt9611c.c b/drivers/gpu/drm/bridge/lontium-lt9611c.c
> new file mode 100644
> index 0000000000000..8fc86c0732122
> --- /dev/null
> +++ b/drivers/gpu/drm/bridge/lontium-lt9611c.c
> @@ -0,0 +1,1361 @@
[ ... ]
> +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;
> +
> + if (!lt9611c)
> + return -ENODEV;
> +
> + dev_warn(dev, "starting firmware upgrade — display will be disrupted\n");
> +
> + ret = lt9611c_firmware_upgrade(lt9611c);
> + if (ret < 0) {
> + dev_err(dev, "upgrade failure\n");
> + return ret;
> + }
> +
> + lt9611c_lock(lt9611c);
> + WRITE_ONCE(lt9611c->fw_version, lt9611c_read_version(lt9611c));
> + lt9611c_unlock(lt9611c);
> +
> + if (READ_ONCE(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",
> + READ_ONCE(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);
> +
> + if (!lt9611c)
> + return -ENODEV;
> +
> + return sysfs_emit(buf, "0x%04x\n", READ_ONCE(lt9611c->fw_version));
> +}
[Severity: Medium]
In drivers/gpu/drm/bridge/lontium-lt9611c.c, what happens in firmware_show()
if reading the version fails after a firmware upgrade in firmware_store()?
When regmap_write() or regmap_bulk_read() fails inside lt9611c_read_version(),
it returns a negative errno that firmware_store() writes directly into
lt9611c->fw_version via WRITE_ONCE().
Subsequent reads of the sysfs firmware attribute will then format that
negative errno with "0x%04x" in firmware_show(), emitting a sign-extended
32-bit hex error code (such as 0xfffffffb) to userspace instead of a 16-bit
firmware version or an error return.
Should firmware_store() only update lt9611c->fw_version when
lt9611c_read_version() succeeds, or should firmware_show() check for a
negative lt9611c->fw_version before formatting it?
[ ... ]
> +static int lt9611c_probe(struct i2c_client *client)
> +{
[ ... ]
> + drm_bridge_add(<9611c->bridge);
> +
> + /* Attach primary DSI */
> + dsi = lt9611c_attach_dsi(lt9611c, dsi0_node);
> + if (IS_ERR(dsi)) {
> + ret = PTR_ERR(dsi);
> + goto err_remove_bridge;
> + }
> +
> + /* Attach secondary DSI, if specified */
> + if (dsi1_node) {
> + dsi = lt9611c_attach_dsi(lt9611c, dsi1_node);
> + if (IS_ERR(dsi)) {
> + ret = PTR_ERR(dsi);
> + goto err_remove_bridge;
> + }
> + }
> +
> + lt9611c->hdmi_connected = false;
> + enable_irq(client->irq);
[Severity: Low]
In lt9611c_probe() in drivers/gpu/drm/bridge/lontium-lt9611c.c, can setting
lt9611c->hdmi_connected = false here race with lt9611c_bridge_detect()?
Once drm_bridge_add() and lt9611c_attach_dsi() have been called, the DSI host
can bind the DRM device and run initial connector detection, which invokes
lt9611c_bridge_detect() and updates lt9611c->hdmi_connected under
lt9611c->mcu_lock:
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;
Writing lt9611c->hdmi_connected = false in lt9611c_probe() without holding
lt9611c->mcu_lock after the bridge is attached can clobber the detected state
(and lt9611c is already zero-initialized by devm_drm_bridge_alloc()). Could
this assignment be dropped or moved before drm_bridge_add()?
[ ... ]
> +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);
> + msleep(400);
> + 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;
> +}
[Severity: Medium]
In drivers/gpu/drm/bridge/lontium-lt9611c.c, should lt9611c_bridge_suspend()
and lt9611c_bridge_resume() hold lt9611c->mcu_lock while asserting reset_gpio,
toggling the regulators, and running lt9611c_reset()?
During system suspend and resume, disable_irq() and cancel_work_sync() only
stop the HPD interrupt and lt9611c->work. Because fw_devlink relaxes
bidirectional OF-graph links and there is no PM device link ordering this I2C
client against the DRM master device or audio codec, bridge callbacks such as
lt9611c_bridge_detect(), lt9611c_bridge_edid_read(),
lt9611c_bridge_atomic_enable(), or lt9611c_hdmi_audio_prepare() can run
concurrently.
Those callbacks serialize hardware access with lt9611c->mcu_lock, whereas
lt9611c_bridge_suspend() and lt9611c_bridge_resume() assert reset_gpio,
disable/enable the vcc and vdd supplies, and run the 400ms reset sequence
without acquiring lt9611c->mcu_lock, which could reset or power down the chip
mid-transaction in lt9611c_read_write_flow().
> +
> +static const struct dev_pm_ops lt9611c_bridge_pm_ops = {
> + SET_SYSTEM_SLEEP_PM_OPS(lt9611c_bridge_suspend,
> + lt9611c_bridge_resume)
> +};
> +
> +static const struct i2c_device_id lt9611c_id[] = {
> + { .name = "lt9611c", .driver_data = (kernel_ulong_t)<9611c_chip_data[CHIP_LT9611C] },
> + { .name = "lt9611ex", .driver_data = (kernel_ulong_t)<9611c_chip_data[CHIP_LT9611EX] },
> + { .name = "lt9611uxd", .driver_data = (kernel_ulong_t)<9611c_chip_data[CHIP_LT9611UXD] },
> + { /* sentinel */ }
> +};
[Severity: Low]
In drivers/gpu/drm/bridge/lontium-lt9611c.c, is MODULE_DEVICE_TABLE(i2c,
lt9611c_id) missing after lt9611c_id?
Without MODULE_DEVICE_TABLE(i2c, lt9611c_id), building the driver as a module
(CONFIG_DRM_LONTIUM_LT9611C=m) omits the i2c:lt9611c, i2c:lt9611ex, and
i2c:lt9611uxd modalias entries from modules.alias, preventing automatic module
loading when devices are instantiated via I2C device IDs.
> +
> +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,
> + },
[Severity: Low]
In drivers/gpu/drm/bridge/lontium-lt9611c.c, will building with
CONFIG_PM_SLEEP=n trigger unused-function warnings for
lt9611c_bridge_suspend() and lt9611c_bridge_resume()?
SET_SYSTEM_SLEEP_PM_OPS() expands to nothing when CONFIG_PM_SLEEP is disabled,
leaving both static functions unreferenced without __maybe_unused annotations.
Could this use DEFINE_SIMPLE_DEV_PM_OPS(lt9611c_bridge_pm_ops,
lt9611c_bridge_suspend, lt9611c_bridge_resume) (or SYSTEM_SLEEP_PM_OPS())
along with pm_sleep_ptr(<9611c_bridge_pm_ops) on .pm so the compiler can
dead-code eliminate the callbacks and dev_pm_ops structure when
CONFIG_PM_SLEEP=n?
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261021-lt9611c-v7-v18-0-4978b94896f1@oss.qualcomm.com?part=2
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH v18 2/2] drm/bridge: Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver
2026-10-21 7:57 ` [PATCH v18 2/2] drm/bridge: " mohit.dsor
2026-10-21 8:23 ` sashiko-bot
@ 2026-10-27 17:57 ` Luca Ceresoli
1 sibling, 0 replies; 5+ messages in thread
From: Luca Ceresoli @ 2026-10-27 17:57 UTC (permalink / raw)
To: mohit.dsor
Cc: 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, Sunyun Yang
Hello,
> LT9611C(EX/UXD) is an I2C-controlled chip that receives single or dual
> port MIPI DSI and outputs 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
>
> The driver supports:
> - EDID read and mode filtering via custom MCU command protocol
> - HPD detection and interrupt handling
> - HDMI audio output via I2S
> - Firmware upgrade via sysfs
> - Dual DSI bonded mode for LT9611EX and LT9611UXD variants
>
> 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>
>
> 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
This should be 7.5 now.
> +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.
Other then debugging, what is the use case for triggering a firmware
upgrade? It seems to creates lots of complications in the code and exposes
and race conditions (see sashiko-reported issues). Maybe starting with a
simpler driver which loads only at probe time would help reviewing.
In case that was discussed previously: sorry, I didn't find the discussion
after a quick look at the (many!) previous iterations, can you point me to
the exact thread?
> +++ b/drivers/gpu/drm/bridge/lontium-lt9611c.c
> @@ -0,0 +1,1361 @@
> +// 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>
Unused?
> +#include <linux/module.h>
> +#include <linux/mutex.h>
> +#include <linux/of_graph.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>
Not sure this is used.
> +#include <drm/drm_edid.h>
> +#include <drm/drm_mipi_dsi.h>
> +#include <drm/drm_modes.h>
> +#include <drm/drm_probe_helper.h>
Unused?
> +#include <drm/display/drm_hdmi_audio_helper.h>
Unused?
> +#include <drm/display/drm_hdmi_state_helper.h>
> +#include <sound/hdmi-codec.h>
> +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 gpio_desc *reset_gpio;
> + struct regulator_bulk_data supplies[2];
> + int fw_version;
> + const struct lt9611c_chip_data *cdata;
> + bool hdmi_connected;
> + bool dual_dsi;
> +};
| sashiko-bot@kernel.org <sashiko-bot@kernel.org>:
|
| [Severity: Low]
| In drivers/gpu/drm/bridge/lontium-lt9611c.c, is MODULE_DEVICE_TABLE(i2c,
| lt9611c_id) missing after lt9611c_id?
|
| Without MODULE_DEVICE_TABLE(i2c, lt9611c_id), building the driver as a module
| (CONFIG_DRM_LONTIUM_LT9611C=m) omits the i2c:lt9611c, i2c:lt9611ex, and
| i2c:lt9611uxd modalias entries from modules.alias, preventing automatic module
| loading when devices are instantiated via I2C device IDs.
Not sure why it's mentiond here, but it's right.
Also, please reply to sashiko remarks either saying you will fix them, or
saying why you won't (e.g. invalid).
> +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);
Can't registers have a #define with a meaningful name? This is valid for th
entire driver.
> +static void lt9611c_lock(struct lt9611c *lt9611c)
> +{
> + mutex_lock(<9611c->mcu_lock);
> + regmap_write(lt9611c->regmap, 0xe0ee, 0x01);
> +}
I'm not sure I get why in some cases you just take mcu_lock and other times
you need this function, which additionally writes a register. Can you
elaborate on that?
> +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;
> +
> + /*
> + * Wait for the MCU to debounce and latch the HPD state after the
> + * interrupt fires. The MCU needs time to update its internal HPD
> + * register; reading immediately after the interrupt returns stale
> + * state. 200 ms is empirically sufficient.
> + */
> + 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, ARRAY_SIZE(lt9611c->supplies), lt9611c->supplies);
> + if (ret)
> + return ret;
> +
> + return regulator_bulk_enable(ARRAY_SIZE(lt9611c->supplies), lt9611c->supplies);
> +}
> +
> +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;
> +}
lt9611c_regulator_init() and lt9611c_attach_dsi() are referenced only by
probe. So please move them just before the probe function, to make it clear
they are part of the probe sequence and they can legitimately use devm.
> +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);
> +
> + if (!(flags & DRM_BRIDGE_ATTACH_NO_CONNECTOR))
> + return -EINVAL;
Good!
> + 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->cdata->max_tmds_rate)
> + return MODE_CLOCK_HIGH;
> +
> + if (tmds_rate < 25000000)
Please #define this number with an informative name,
e.g. LT9611C_MIX_TMDS_RATE (not sure it's correct).
> + 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;
> + struct {
> + __be16 htotal;
> + __be16 hactive;
> + __be16 hfront_porch;
> + __be16 hsync_len;
> + __be16 hback_porch;
> + __be16 vtotal;
> + __be16 vactive;
> + __be16 vfront_porch;
> + __be16 vsync_len;
> + __be16 vback_porch;
> + u8 framerate;
> + u8 vic;
> + } timing_data;
> + struct lt9611c_rsp rsp = {};
> + struct lt9611c_cmd cmd = {
> + .hdr = { LT9611C_FUNC_WRITE, LT9611C_TYPE_MIPI, 0x33, LT9611C_CMD_SEP },
> + .data = (u8 *)&timing_data,
> + .data_len = sizeof(timing_data),
> + };
> +
> + timing_data.htotal = cpu_to_be16(mode->htotal);
> + timing_data.hactive = cpu_to_be16(mode->hdisplay);
> + timing_data.hfront_porch = cpu_to_be16(mode->hsync_start - mode->hdisplay);
> + timing_data.hsync_len = cpu_to_be16(mode->hsync_end - mode->hsync_start);
> + timing_data.hback_porch = cpu_to_be16(mode->htotal - mode->hsync_end);
> + timing_data.vtotal = cpu_to_be16(mode->vtotal);
> + timing_data.vactive = cpu_to_be16(mode->vdisplay);
> + timing_data.vfront_porch = cpu_to_be16(mode->vsync_start - mode->vdisplay);
> + timing_data.vsync_len = cpu_to_be16(mode->vsync_end - mode->vsync_start);
> + timing_data.vback_porch = cpu_to_be16(mode->vtotal - mode->vsync_end);
> + timing_data.framerate = drm_mode_vrefresh(mode);
> + timing_data.vic = drm_match_cea_mode(mode);
> +
> + dev_dbg(dev, "hactive=%d, vactive=%d\n", mode->hdisplay, mode->vdisplay);
> + dev_dbg(dev, "framerate=%d\n", timing_data.framerate);
> + dev_dbg(dev, "vic = 0x%02x\n", timing_data.vic);
> +
> + guard(mutex)(<9611c->mcu_lock);
> +
> + ret = lt9611c_read_write_flow(lt9611c, &cmd, &rsp);
> + if (ret)
> + dev_err(dev, "video set failed\n");
> +}
> +
> +/*
> + * Enables dual DSI port input on the LT9611UXD/EX. Must be called
> + * after video_setup when two DSI hosts are connected.
> + */
> +static int lt9611c_dual_port_enable(struct lt9611c *lt9611c)
> +{
> + static const u8 dual_port_data[] = { 0x02, 0x70 };
> + struct device *dev = lt9611c->dev;
> + u8 ack = 0;
> + int ret;
> + struct lt9611c_rsp rsp = { .data = &ack, .data_len = 1 };
> + struct lt9611c_cmd cmd = {
> + .hdr = { LT9611C_FUNC_WRITE, LT9611C_TYPE_MIPI, 0x31, LT9611C_CMD_SEP },
> + .data = dual_port_data,
> + .data_len = ARRAY_SIZE(dual_port_data),
> + };
> +
> + guard(mutex)(<9611c->mcu_lock);
> +
> + ret = lt9611c_read_write_flow(lt9611c, &cmd, &rsp);
> + if (ret || !ack) {
> + dev_err(dev, "failed to enable dual DSI port (err=%d)\n", ret);
> + return ret ? ret : -EIO;
> + }
> +
> + return 0;
> +}
> +
> +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;
> + int ret;
> +
> + 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);
> +
> + if (lt9611c->dual_dsi) {
> + ret = lt9611c_dual_port_enable(lt9611c);
> + if (ret)
> + dev_err(lt9611c->dev, "failed to enable dual DSI port (err=%d)\n", ret);
> + }
lt9611c_video_setup() takes and releases mcu_lock, and
lt9611c_dual_port_enable() take it again, so there is a possible race
window. I'm not sure it can cause problems, but these two functions are
never called separately, so whay not folding lt9611c_dual_port_enable()
into lt9611c_video_setup()? Or at least calling lt9611c_dual_port_enable()
from lt9611c_video_setup(), so callers (or, well, the unique caller) don't
have to take care about dual DSI?
> +
> + drm_atomic_helper_connector_hdmi_update_infoframes(connector, state);
> +}
> +
> +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;
> +}
This code is very similar to lt9611c_hpd_work(), can the common code be
moved to an internal function? It might take the msleep timeout as a
parameter, as it seems to be the only real difference.
BTW is the timeout really never necessary here? Even if
lt9611c_bridge_detect() is called by chance just after an interrupt?
> +static int lt9611c_parse_dt(struct device *dev,
> + struct lt9611c *lt9611c,
> + struct device_node **dsi0_node,
> + struct device_node **dsi1_node)
> +{
> + int ret;
> +
> + *dsi0_node = of_graph_get_remote_node(dev->of_node, 0, -1);
> + if (!*dsi0_node)
> + return dev_err_probe(dev, -ENODEV, "failed to get remote node for primary dsi\n");
> +
> + *dsi1_node = of_graph_get_remote_node(dev->of_node, 1, -1);
> +
> + if (*dsi1_node && lt9611c->cdata->chip_type == CHIP_LT9611C) {
> + ret = dev_err_probe(dev, -EINVAL,
> + "LT9611C does not support dual DSI\n");
> + goto err_put_dsi1;
> + }
> +
> + if (*dsi1_node)
> + lt9611c->dual_dsi = true;
> +
> + 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(*dsi1_node);
> + of_node_put(*dsi0_node);
> + return ret;
> +}
> +
> +static int lt9611c_read_version(struct lt9611c *lt9611c)
Maybe rename to lt9611c_read_fw_version() for clarity?
> +{
> + 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 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;
> +
> + if (!lt9611c)
> + return -ENODEV;
> +
> + dev_warn(dev, "starting firmware upgrade — display will be disrupted\n");
> +
> + ret = lt9611c_firmware_upgrade(lt9611c);
> + if (ret < 0) {
> + dev_err(dev, "upgrade failure\n");
> + return ret;
> + }
> +
> + lt9611c_lock(lt9611c);
> + WRITE_ONCE(lt9611c->fw_version, lt9611c_read_version(lt9611c));
> + lt9611c_unlock(lt9611c);
> +
> + if (READ_ONCE(lt9611c->fw_version) < 0)
> + dev_warn(dev, "upgrade succeeded but failed to read new fw version\n");
Logging the errno (lt9611c->fw_version) could be useful here.
> +static int lt9611c_probe(struct i2c_client *client)
> +{
> + struct lt9611c *lt9611c;
> + struct device *dev = &client->dev;
> + struct device_node *dsi0_node = NULL;
> + struct device_node *dsi1_node = NULL;
Small improvement: using a cleanup action would simplify a bit the error
management, removing the err_put_nodes label here and the err_put_dsi1
label in lt9611c_parse_dt():
struct device_node *dsi0_node __free(device_node) = NULL;
> + struct mipi_dsi_device *dsi;
> + bool fw_updated = false;
> + int ret;
> +
> + 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;
> + i2c_set_clientdata(client, lt9611c);
> +
> + const struct lt9611c_chip_data *cdata = i2c_get_match_data(client);
> +
> + if (!cdata)
> + return dev_err_probe(dev, -EINVAL, "no match data for device\n");
Minor nit: no newline between variable assignment and error checking.
> +
> + lt9611c->cdata = cdata;
> +
> + 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, &dsi0_node, &dsi1_node);
> + 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_put_nodes;
> + }
> +
> + ret = lt9611c_regulator_init(lt9611c);
> + if (ret < 0)
> + goto err_put_nodes;
> +
> + 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;
lt9611c_read_version() returns an error code from regmap functions. Why are
you ignoring it and forcing -EOPNOTSUPP?
Also, dev_err_probe would be better here:
if (lt9611c->fw_version < 0) {
ret = lt9611c->fw_version
dev_err_probe(dev, ret, "failed to read fw version\n");
lt9611c_unlock(lt9611c);
goto err_disable_regulators;
} else ...
> + } 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;
> + }
> + }
Ugh, a goto-based loop in the middle of a very long function is not helping
readability. The FW loading logic would be better moved to its own
function.
> +
> + 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;
> +
> + drm_bridge_add(<9611c->bridge);
> +
> + /* Attach primary DSI */
> + dsi = lt9611c_attach_dsi(lt9611c, dsi0_node);
> + if (IS_ERR(dsi)) {
> + ret = PTR_ERR(dsi);
> + goto err_remove_bridge;
> + }
> +
> + /* Attach secondary DSI, if specified */
> + if (dsi1_node) {
> + dsi = lt9611c_attach_dsi(lt9611c, dsi1_node);
> + if (IS_ERR(dsi)) {
> + ret = PTR_ERR(dsi);
> + goto err_remove_bridge;
> + }
> + }
> +
> + lt9611c->hdmi_connected = false;
> + enable_irq(client->irq);
> +
> + of_node_put(dsi1_node);
> + of_node_put(dsi0_node);
> +
> + return 0;
> +
> +err_remove_bridge:
> + drm_bridge_remove(<9611c->bridge);
> + cancel_work_sync(<9611c->work);
> +
> +err_disable_regulators:
> + regulator_bulk_disable(ARRAY_SIZE(lt9611c->supplies), lt9611c->supplies);
> +
> +err_put_nodes:
> + of_node_put(dsi1_node);
> + of_node_put(dsi0_node);
> +
> + return ret;
> +}
> +static struct i2c_driver lt9611c_driver = {
> + .driver = {
> + .name = "lt9611c",
> + .of_match_table = lt9611c_match_table,
> + .pm = <9611c_bridge_pm_ops,
As suggested by sashiko, please use the new DEFINE_SIMPLE_DEV_PM_OPS()
macro or similar to avoid "defined but unused" warnings. Also test your
driver in all combinations of DRM_LONTIUM_LT9611C=y/m and PM_SLEEP=y/n.
Luca
--
Luca Ceresoli, Bootlin
Embedded Linux and Kernel engineering
https://bootlin.com
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-10-27 17:58 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-21 7:57 [PATCH v18 0/2] Add Lontium LT9611C(EX/UXD) MIPI DSI to HDMI driver mohit.dsor
2026-10-21 7:57 ` [PATCH v18 1/2] dt-bindings: bridge: " mohit.dsor
2026-10-21 7:57 ` [PATCH v18 2/2] drm/bridge: " mohit.dsor
2026-10-21 8:23 ` sashiko-bot
2026-10-27 17:57 ` Luca Ceresoli
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.