* [PATCH v7 0/2] This series adds LT9211C bridge driver by extending LT9211
@ 2026-08-22 17:01 vishnu.saini
2026-08-22 17:01 ` [PATCH v7 1/2] dt-bindings: display: bridge: lontium,lt9211: Add lt9211c support vishnu.saini
2026-08-22 17:01 ` [PATCH v7 2/2] drm/bridge: add support for lontium lt9211c bridge vishnu.saini
0 siblings, 2 replies; 4+ messages in thread
From: vishnu.saini @ 2026-08-22 17:01 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, Marek Vasut
Cc: dri-devel, devicetree, linux-kernel, Yi Zhang, Nilesh Laad,
Gopi Botlagunta, Vishnu Saini, venkata.valluru, Jessica Zhang,
Philipp Zabel
LT9211c is a Single/Dual-Link DSI/LVDS or Single DPI input to
Single-link/Dual-Link DSI/LVDS or Single DPI output bridge chip.
This adds support for DSI to LVDS bridge configuration. By extending the
existing lontium-lt9211 driver to support DSI-to-LVDS bridge configuration.
Signed-off-by: Yi Zhang <zhanyi@qti.qualcomm.com>
Signed-off-by: Nilesh Laad <nilesh.laad@oss.qualcomm.com>
Signed-off-by: Gopi Botlagunta <venkata.botlagunta@oss.qualcomm.com>
Signed-off-by: Vishnu Saini <vishnu.saini@oss.qualcomm.com>
---
Changes in v7:
- Squashed 5 fixes from Philipp Zabel, except drop delayed work.
drm/bridge: lt9211: fix regmap_write error handling
drm/bridge: lt9211: use regmap_write_bits
drm/bridge: lt9211: reuse lt9211_configure_timing in lt9211c_configure_timing
drm/bridge: lt9211: combine equal branches
drm/bridge: lt9211: use devm_drm_bridge_add and drop lt9211_remove
- Keeping the delayed work but improved the approach by reducing the delay
to 0ms and wait on vblank irq to correctly configure bridge and autodetect
RX as with synchronous approach in atomic_enable callback autodetect RX
was failed to detect RX format/width/height due to stream was not live.
- Introduced lt9211_chip_data and fetched it via i2c_get_match_data(),
removing the remaining chip_type branches. [Philipp Zabel]
- Switched lt9211c_regmap_config.cache_type from REGCACHE_RBTREE to
REGCACHE_NONE. [Philipp Zabel]
- The few read-modify-write registers now use regmap_write_bits()) [Philipp Zabel]
- Merged the mode->clock < 22000 and < 44000 branches in
lt9211c_configure_plls() that wrote identical 0x822f/div values;
0x822c is now written unconditionally in every branch instead of
being left stale when the mode clock moves out of the < 22000 range [Philipp Zabel]
- Removed the dead "set bit 0x80 then mask it off" on the 0xd026
write in lt9211c_configure_plls() [Philipp Zabel]
- Consolidated the duplicated SOT / HS-settle-set write into a single
hs_settle calculation and one regmap_write() call [Philipp Zabel]
- Fixed error handling on regmap_write() calls in
lt9211c_configure_plls() that used "ret |=" across multiple writes,
which drops the return value of an earlier failed write [Philipp Zabel]
- Reused lt9211_configure_timing() from lt9211c_configure_timing()
instead of duplicating the timing register sequence [Philipp Zabel]
- Switched to devm_drm_bridge_add() and devm_add_action_or_reset() for
the LT9211C workqueue, dropping the explicit lt9211_remove() [Philipp Zabel]
- Squashed LT9211C burst mode fix from Oleksij Rempel.
- Added Tested-by: Philipp Zabel <p.zabel@pengutronix.de>
- Link to v6: https://lore.kernel.org/r/20260719-add-lt9211c-bridge-v6-0-e56d22152586@oss.qualcomm.com
Changes in v6:
- Nilesh Laad and Gopi Botlagunta moved to different org in Qualcomm. Further, i will followup on this series with community, will also submit the dt series where LT9211C bridge is used.
- Rebased onto current drm-misc-next (commit 671b7825dbfe)
- Fixed lt9211_read_chipid: LT9211 match no longer checks chipid[2],
preserving compatibility with LT9211 rev U5 (db74b04edce1)
- Fixed indentation in lt9211_atomic_enable LT9211 path (was double-indented)
- Fixed indentation in lt9211_delayed_work_func body (was double-indented)
- Gated workqueue creation on LT9211C chip type only
- Fixed few formatting warnings with coccinelle and W=1
- Link to v5: https://lore.kernel.org/r/20260323-add-lt9211c-bridge-v5-0-9c63bb035c17@oss.qualcomm.com
Changes in v5:
- Addressed code formatting in lt9211 driver (no functional or design changes)
- Addressed v4 comments on lontium-lt9211.yaml
- Link to v4: https://lore.kernel.org/r/20251224-add-lt9211c-bridge-v4-0-406e73ec28c5@oss.qualcomm.com
Changes in v4:
- Removed lontium-lt9211c.yaml
- Extended lontium-lt9211.yaml to support LT9211C
- Link to v3: https://lore.kernel.org/r/20251218-add-lt9211c-bridge-v3-0-1ee0670a0db2@oss.qualcomm.com
Changes in v3:
- Removed lontium-lt9211c as separate driver
- Add support to lontium-lt9211c bridge driver by extending the existing lontium-lt9211.c
- Fixed kernel test robot reported build errors
- Link to v2: https://lore.kernel.org/lkml/20251107-add-lt9211c-bridge-v2-0-b0616e23407c@oss.qualcomm.com/
Changes in v2:
- Combined driver patch from https://lore.kernel.org/lkml/20250911-lt9211c-bridge-support-v1-1-c221202cbcd5@oss.qualcomm.com/
- Added MODULE_AUTHOR
- Uppercase to lowercase for hex values
- Link to v1: https://lore.kernel.org/r/20250910-add-lt9211c-bridge-v1-1-4f23740fe101@oss.qualcomm.com
---
Yi Zhang (2):
dt-bindings: display: bridge: lontium,lt9211: Add lt9211c support
drm/bridge: add support for lontium lt9211c bridge
.../bindings/display/bridge/lontium,lt9211.yaml | 5 +-
drivers/gpu/drm/bridge/lontium-lt9211.c | 838 ++++++++++++++++++++-
2 files changed, 808 insertions(+), 35 deletions(-)
---
base-commit: 063019ac51afffda4ab3d658701b086caa0c8e9e
change-id: 20250910-add-lt9211c-bridge-5a21fcb1c803
Best regards,
--
Vishnu Saini <vishnu.saini@oss.qualcomm.com>
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH v7 1/2] dt-bindings: display: bridge: lontium,lt9211: Add lt9211c support
2026-08-22 17:01 [PATCH v7 0/2] This series adds LT9211C bridge driver by extending LT9211 vishnu.saini
@ 2026-08-22 17:01 ` vishnu.saini
2026-08-22 17:01 ` [PATCH v7 2/2] drm/bridge: add support for lontium lt9211c bridge vishnu.saini
1 sibling, 0 replies; 4+ messages in thread
From: vishnu.saini @ 2026-08-22 17:01 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, Marek Vasut
Cc: dri-devel, devicetree, linux-kernel, Yi Zhang, Nilesh Laad,
Gopi Botlagunta, Vishnu Saini, venkata.valluru, Jessica Zhang
From: Yi Zhang <zhanyi@qti.qualcomm.com>
Add binding for the Lontium LT9211C bridge chip.
Signed-off-by: Yi Zhang <zhanyi@qti.qualcomm.com>
Signed-off-by: Nilesh Laad <nilesh.laad@oss.qualcomm.com>
Signed-off-by: Gopi Botlagunta <venkata.botlagunta@oss.qualcomm.com>
Reviewed-by: Rob Herring (Arm) <robh@kernel.org>
Signed-off-by: Vishnu Saini <vishnu.saini@oss.qualcomm.com>
---
Documentation/devicetree/bindings/display/bridge/lontium,lt9211.yaml | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/Documentation/devicetree/bindings/display/bridge/lontium,lt9211.yaml b/Documentation/devicetree/bindings/display/bridge/lontium,lt9211.yaml
index 7cfe92a8bcd7..de61bda09db9 100644
--- a/Documentation/devicetree/bindings/display/bridge/lontium,lt9211.yaml
+++ b/Documentation/devicetree/bindings/display/bridge/lontium,lt9211.yaml
@@ -4,19 +4,20 @@
$id: http://devicetree.org/schemas/display/bridge/lontium,lt9211.yaml#
$schema: http://devicetree.org/meta-schemas/core.yaml#
-title: Lontium LT9211 DSI/LVDS/DPI to DSI/LVDS/DPI bridge.
+title: Lontium LT9211/LT9211C DSI/LVDS/DPI to DSI/LVDS/DPI bridge.
maintainers:
- Marek Vasut <marex@denx.de>
description: |
- The LT9211 are bridge devices which convert Single/Dual-Link DSI/LVDS
+ The LT9211 and LT9211C are bridge devices which convert Single/Dual-Link DSI/LVDS
or Single DPI to Single/Dual-Link DSI/LVDS or Single DPI.
properties:
compatible:
enum:
- lontium,lt9211
+ - lontium,lt9211c
reg:
maxItems: 1
--
2.34.1
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH v7 2/2] drm/bridge: add support for lontium lt9211c bridge
2026-08-22 17:01 [PATCH v7 0/2] This series adds LT9211C bridge driver by extending LT9211 vishnu.saini
2026-08-22 17:01 ` [PATCH v7 1/2] dt-bindings: display: bridge: lontium,lt9211: Add lt9211c support vishnu.saini
@ 2026-08-22 17:01 ` vishnu.saini
2026-08-22 17:15 ` sashiko-bot
1 sibling, 1 reply; 4+ messages in thread
From: vishnu.saini @ 2026-08-22 17:01 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, Marek Vasut
Cc: dri-devel, devicetree, linux-kernel, Yi Zhang, Nilesh Laad,
Gopi Botlagunta, Vishnu Saini, venkata.valluru, Jessica Zhang,
Philipp Zabel
From: Yi Zhang <zhanyi@qti.qualcomm.com>
LT9211C is a Single/Dual-Link DSI/LVDS or Single DPI input to
Single-Link/Dual-Link DSI/LVDS or Single DPI output bridge chip.
Extend the existing lontium-lt9211 driver to support DSI-to-LVDS
bridge configuration by detecting and handling both LT9211 and LT9211C
variants from a single driver.
Chip detection in lt9211_read_chipid() is extended to identify the
LT9211C by its distinct chip ID registers.
Add LT9211C-specific regmap support and use lt9211_chip_data with
i2c_get_match_data() to provide per-chip configuration.
Five new functions implement the LT9211C DSI-to-LVDS initialisation
sequence: lt9211c_configure_rx(), lt9211c_autodetect_rx(),
lt9211c_configure_timing(), lt9211c_configure_plls() and
lt9211c_configure_tx().
Defer the remaining LT9211C initialization to a delayed work item
scheduled from atomic_enable(), since RX auto-detection requires an
active DSI stream.
Signed-off-by: Yi Zhang <zhanyi@qti.qualcomm.com>
Signed-off-by: Nilesh Laad <nilesh.laad@oss.qualcomm.com>
Signed-off-by: Gopi Botlagunta <venkata.botlagunta@oss.qualcomm.com>
Signed-off-by: Vishnu Saini <vishnu.saini@oss.qualcomm.com>
Tested-by: Philipp Zabel <p.zabel@pengutronix.de>
---
drivers/gpu/drm/bridge/lontium-lt9211.c | 838 ++++++++++++++++++++++++++++++--
1 file changed, 805 insertions(+), 33 deletions(-)
diff --git a/drivers/gpu/drm/bridge/lontium-lt9211.c b/drivers/gpu/drm/bridge/lontium-lt9211.c
index 23682384daf4..d26789b610c1 100644
--- a/drivers/gpu/drm/bridge/lontium-lt9211.c
+++ b/drivers/gpu/drm/bridge/lontium-lt9211.c
@@ -8,6 +8,7 @@
* 1xDSI -> 1xLVDS
*
* Copyright (C) 2022 Marek Vasut <marex@denx.de>
+ * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
*/
#include <linux/bits.h>
@@ -19,6 +20,7 @@
#include <linux/of_graph.h>
#include <linux/regmap.h>
#include <linux/regulator/consumer.h>
+#include <linux/workqueue.h>
#include <drm/drm_atomic_helper.h>
#include <drm/drm_bridge.h>
@@ -27,6 +29,7 @@
#include <drm/drm_panel.h>
#include <drm/drm_print.h>
#include <drm/drm_probe_helper.h>
+#include <drm/drm_vblank.h>
#define REG_PAGE_CONTROL 0xff
#define REG_CHIPID0 0x8100
@@ -36,10 +39,28 @@
#define REG_CHIPID2 0x8102
#define REG_CHIPID2_VALUE 0xe3
+/* LT9211C chip ID values */
+#define REG_CHIPID0_LT9211C_VALUE 0x21
+#define REG_CHIPID1_LT9211C_VALUE 0x03
+#define REG_CHIPID2_LT9211C_VALUE 0xe1
+
#define REG_DSI_LANE 0xd000
/* DSI lane count - 0 means 4 lanes ; 1, 2, 3 means 1, 2, 3 lanes. */
#define REG_DSI_LANE_COUNT(n) ((n) & 3)
+/* Chip type enum */
+enum lt9211_chip_type {
+ LT9211,
+ LT9211C,
+};
+
+struct lt9211_chip_data {
+ enum lt9211_chip_type chip_type;
+ const struct regmap_config *regmap_config;
+ const char *dsi_type;
+ unsigned long dsi_mode_flags;
+};
+
struct lt9211 {
struct drm_bridge bridge;
struct device *dev;
@@ -50,6 +71,15 @@ struct lt9211 {
struct regulator *vccio;
bool lvds_dual_link;
bool lvds_dual_link_even_odd_swap;
+ const struct lt9211_chip_data *chip_data;
+ /* LT9211C specific fields */
+ struct workqueue_struct *wq;
+ struct delayed_work lt9211_dw;
+ struct drm_crtc *lt9211c_crtc;
+ struct drm_display_mode lt9211c_mode;
+ bool lt9211c_bpp24;
+ bool lt9211c_jeida;
+ bool lt9211c_de_high;
};
static const struct regmap_range lt9211_rw_ranges[] = {
@@ -93,6 +123,65 @@ static const struct regmap_config lt9211_regmap_config = {
.max_register = 0xda00,
};
+static const struct regmap_range lt9211c_rw_ranges[] = {
+ regmap_reg_range(0xff, 0xff),
+ regmap_reg_range(0x8100, 0x8182),
+ regmap_reg_range(0x8200, 0x82aa),
+ regmap_reg_range(0x8500, 0x85ff),
+ regmap_reg_range(0x8600, 0x86a0),
+ regmap_reg_range(0x8700, 0x8746),
+ regmap_reg_range(0xd000, 0xd0a7),
+ regmap_reg_range(0xd400, 0xd42c),
+ regmap_reg_range(0xd800, 0xd838),
+ regmap_reg_range(0xd9c0, 0xd9d5),
+};
+
+static const struct regmap_access_table lt9211c_rw_table = {
+ .yes_ranges = lt9211c_rw_ranges,
+ .n_yes_ranges = ARRAY_SIZE(lt9211c_rw_ranges),
+};
+
+static const struct regmap_range_cfg lt9211c_range = {
+ .name = "lt9211c",
+ .range_min = 0x0000,
+ .range_max = 0xda00,
+ .selector_reg = REG_PAGE_CONTROL,
+ .selector_mask = 0xff,
+ .selector_shift = 0,
+ .window_start = 0,
+ .window_len = 0x100,
+};
+
+static const struct regmap_config lt9211c_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .rd_table = <9211c_rw_table,
+ .wr_table = <9211c_rw_table,
+ .volatile_table = <9211c_rw_table,
+ .ranges = <9211c_range,
+ .num_ranges = 1,
+ .cache_type = REGCACHE_NONE,
+ .max_register = 0xda00,
+};
+
+static const struct lt9211_chip_data lt9211_chip_data = {
+ .chip_type = LT9211,
+ .regmap_config = <9211_regmap_config,
+ .dsi_type = "lt9211",
+ .dsi_mode_flags = MIPI_DSI_MODE_VIDEO | MIPI_DSI_MODE_VIDEO_SYNC_PULSE |
+ MIPI_DSI_MODE_VIDEO_HSE | MIPI_DSI_MODE_VIDEO_NO_HSA |
+ MIPI_DSI_MODE_VIDEO_NO_HFP | MIPI_DSI_MODE_VIDEO_NO_HBP |
+ MIPI_DSI_MODE_NO_EOT_PACKET,
+};
+
+static const struct lt9211_chip_data lt9211c_chip_data = {
+ .chip_type = LT9211C,
+ .regmap_config = <9211c_regmap_config,
+ .dsi_type = "lt9211c",
+ .dsi_mode_flags = MIPI_DSI_MODE_VIDEO | MIPI_DSI_MODE_VIDEO_BURST |
+ MIPI_DSI_MODE_LPM,
+};
+
static struct lt9211 *bridge_to_lt9211(struct drm_bridge *bridge)
{
return container_of(bridge, struct lt9211, bridge);
@@ -120,14 +209,23 @@ static int lt9211_read_chipid(struct lt9211 *ctx)
return ret;
}
- /* Test for known Chip ID. */
- if (chipid[0] != REG_CHIPID0_VALUE || chipid[1] != REG_CHIPID1_VALUE) {
- dev_err(ctx->dev, "Unknown Chip ID: 0x%02x 0x%02x 0x%02x\n",
- chipid[0], chipid[1], chipid[2]);
- return -EINVAL;
+ /* Test for LT9211 Chip ID. */
+ if (chipid[0] == REG_CHIPID0_VALUE && chipid[1] == REG_CHIPID1_VALUE) {
+ dev_dbg(ctx->dev, "Detected LT9211 chip\n");
+ return 0;
}
- return 0;
+ /* Test for LT9211C Chip ID. */
+ if (chipid[0] == REG_CHIPID0_LT9211C_VALUE &&
+ chipid[1] == REG_CHIPID1_LT9211C_VALUE &&
+ chipid[2] == REG_CHIPID2_LT9211C_VALUE) {
+ dev_dbg(ctx->dev, "Detected LT9211C chip\n");
+ return 0;
+ }
+
+ dev_err(ctx->dev, "Unknown Chip ID: 0x%02x 0x%02x 0x%02x\n",
+ chipid[0], chipid[1], chipid[2]);
+ return -EINVAL;
}
static int lt9211_system_init(struct lt9211 *ctx)
@@ -454,6 +552,657 @@ static int lt9211_configure_tx(struct lt9211 *ctx, bool jeida,
return 0;
}
+static int lt9211c_configure_rx(struct lt9211 *ctx)
+{
+ unsigned int pval;
+
+ const struct reg_sequence lt9211c_rx_phy_seq[] = {
+ { REG_DSI_LANE, REG_DSI_LANE_COUNT(ctx->dsi->lanes) },
+ { 0x8201, 0x11 },
+ { 0x8218, 0x48 },
+ { 0x8201, 0x91 },
+ { 0x8202, 0x00 },
+ { 0x8203, 0xee },
+ { 0x8209, 0x21 },
+ { 0x8204, 0x44 },
+ { 0x8205, 0xc4 },
+ { 0x8206, 0x44 },
+ { 0x8213, 0x0c },
+
+ { 0xd001, 0x00 },
+ { 0xd002, 0x0e },
+ { 0xd005, 0x00 },
+ { 0xd00a, 0x59 },
+ { 0xd00b, 0x20 },
+ };
+
+ const struct reg_sequence lt9211c_rx_phy_reset_seq[] = {
+ { 0x8109, 0xde },
+ { 0x8109, 0xdf },
+ };
+
+ const struct reg_sequence lt9211c_rx_clk_sel_seq[] = {
+ { 0x85e9, 0x88 },
+ { 0x8180, 0x51 },
+ { 0x8181, 0x10 },
+ { 0x8632, 0x03 },
+ };
+
+ const struct reg_sequence lt9211c_rx_input_sel_seq[] = {
+ { 0xd004, 0x00 },
+ { 0xd021, 0x46 },
+ };
+
+ const struct reg_sequence lt9211c_rx_dig_seq[] = {
+ { 0x853f, 0x08 },
+ { 0x8540, 0x04 },
+ { 0x8541, 0x03 },
+ { 0x8542, 0x02 },
+ { 0x8543, 0x01 },
+ { 0x8545, 0x04 },
+ { 0x8546, 0x03 },
+ { 0x8547, 0x02 },
+ { 0x8548, 0x01 },
+ { 0x8544, 0x00 },
+ { 0x8549, 0x00 },
+ };
+
+ int ret;
+
+ ret = regmap_multi_reg_write(ctx->regmap, lt9211c_rx_phy_seq,
+ ARRAY_SIZE(lt9211c_rx_phy_seq));
+ if (ret) {
+ dev_err(ctx->dev, "RX: phy_seq write failed: %d\n", ret);
+ return ret;
+ }
+
+ ret = regmap_multi_reg_write(ctx->regmap, lt9211c_rx_phy_reset_seq,
+ ARRAY_SIZE(lt9211c_rx_phy_reset_seq));
+ if (ret) {
+ dev_err(ctx->dev, "RX: phy_reset_seq write failed: %d\n", ret);
+ return ret;
+ }
+
+ ret = regmap_multi_reg_write(ctx->regmap, lt9211c_rx_clk_sel_seq,
+ ARRAY_SIZE(lt9211c_rx_clk_sel_seq));
+ if (ret) {
+ dev_err(ctx->dev, "RX: clk_sel_seq write failed: %d\n", ret);
+ return ret;
+ }
+
+ ret = regmap_write_bits(ctx->regmap, 0x8180, 0x3, 0x3);
+ if (ret)
+ return ret;
+
+ ret = regmap_read(ctx->regmap, 0x8680, &pval);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(ctx->regmap, 0x863f, (pval & 0xf8));
+ if (ret)
+ return ret;
+
+ ret = regmap_write(ctx->regmap, 0x863f, 0x05);
+ if (ret)
+ return ret;
+
+ ret = regmap_write_bits(ctx->regmap, 0x8530, 0x17, 0x11);
+ if (ret)
+ return ret;
+
+ ret = regmap_multi_reg_write(ctx->regmap, lt9211c_rx_input_sel_seq,
+ ARRAY_SIZE(lt9211c_rx_input_sel_seq));
+ if (ret) {
+ dev_err(ctx->dev, "RX: input_sel_seq write failed: %d\n", ret);
+ return ret;
+ }
+
+ ret = regmap_multi_reg_write(ctx->regmap, lt9211c_rx_dig_seq,
+ ARRAY_SIZE(lt9211c_rx_dig_seq));
+ if (ret) {
+ dev_err(ctx->dev, "RX: dig_seq write failed: %d\n", ret);
+ return ret;
+ }
+
+ /* Give the chip time to lock onto RX stream. */
+ msleep(100);
+
+ return 0;
+}
+
+static int lt9211c_autodetect_rx(struct lt9211 *ctx,
+ const struct drm_display_mode *mode)
+{
+ u16 width, height;
+ u8 buf[5];
+ u8 format;
+ u8 sot[8];
+ u8 hs_settle;
+ int ret;
+
+ /* Read the SOT from the chip. */
+ ret = regmap_bulk_read(ctx->regmap, 0xd088, sot, sizeof(sot));
+ if (ret) {
+ dev_err(ctx->dev, "RX: failed to read SOT (0xd088): %d\n", ret);
+ return ret;
+ }
+
+ dev_dbg(ctx->dev, "Sot Num = 0x%02x, 0x%02x, 0x%02x, 0x%02x\n",
+ sot[0], sot[2], sot[4], sot[6]);
+
+ dev_dbg(ctx->dev, "Sot Data = 0x%02x, 0x%02x, 0x%02x, 0x%02x\n",
+ sot[1], sot[3], sot[5], sot[7]);
+
+ /* HS Settle Set */
+ hs_settle = (sot[0] > 0x10 && sot[0] < 0x50) ? sot[0] - 5 : 0x08;
+ ret = regmap_write(ctx->regmap, 0xd002, hs_settle);
+ if (ret)
+ return ret;
+
+ /* Width/Height/Format Auto-detection */
+ ret = regmap_bulk_read(ctx->regmap, 0xd082, buf, sizeof(buf));
+ if (ret)
+ return ret;
+
+ width = (buf[0] << 8) | buf[1];
+ height = (buf[3] << 8) | buf[4];
+ format = buf[2] & 0xf;
+
+ if (format == 0x3) { /* YUV422 16bit */
+ width /= 2;
+ } else if (format == 0xa) { /* RGB888 24bit */
+ width /= 3;
+ } else {
+ dev_err(ctx->dev,
+ "Unsupported DSI format 0x%01x (0xd082=%02x %02x %02x %02x %02x)\n",
+ format, buf[0], buf[1], buf[2], buf[3], buf[4]);
+ return -EINVAL;
+ }
+
+ if (width != mode->hdisplay) {
+ dev_err(ctx->dev,
+ "RX: Detected DSI width (%d) does not match mode hdisplay (%d)\n",
+ width, mode->hdisplay);
+ return -EINVAL;
+ }
+
+ if (height != mode->vdisplay) {
+ dev_err(ctx->dev,
+ "RX: Detected DSI height (%d) does not match mode vdisplay (%d)\n",
+ height, mode->vdisplay);
+ return -EINVAL;
+ }
+
+ dev_dbg(ctx->dev, "RX: %dx%d format=0x%01x\n", width, height, format);
+
+ return 0;
+}
+
+static int lt9211c_configure_timing(struct lt9211 *ctx,
+ const struct drm_display_mode *mode)
+{
+ int ret;
+
+ ret = lt9211_configure_timing(ctx, mode);
+ if (ret < 0)
+ return ret;
+
+ return regmap_write(ctx->regmap, 0xd04c,
+ ((mode->hsync_end - mode->hsync_start) >> 8) & 0xff);
+}
+
+static int lt9211c_configure_plls(struct lt9211 *ctx,
+ const struct drm_display_mode *mode)
+{
+ const struct reg_sequence lt9211c_dessc_pll_reset[] = {
+ { 0x8103, 0xfe, 2000 },
+ { 0x8103, 0xff, 0 },
+ };
+
+ const struct reg_sequence lt9211c_pcr_cali_seq[] = {
+ { 0xd00a, 0x5f },
+ { 0xd01e, 0x51 },
+ { 0xd023, 0x80 },
+ { 0xd024, 0x70 },
+ { 0xd025, 0x80 },
+ { 0xd02a, 0x10 },
+ { 0xd021, 0x4f },
+ { 0xd022, 0xf0 },
+ { 0xd038, 0x04 },
+ { 0xd039, 0x08 },
+ { 0xd03a, 0x10 },
+ { 0xd03b, 0x20 },
+ { 0xd03f, 0x04 },
+ { 0xd040, 0x08 },
+ { 0xd041, 0x10 },
+ { 0xd042, 0x20 },
+ { 0xd02b, 0xa0 },
+ };
+
+ const struct reg_sequence lt9211c_pcr_reset_seq[] = {
+ { 0xd009, 0xdb },
+ { 0xd009, 0xdf },
+ { 0xd008, 0x80 },
+ { 0xd008, 0x00 },
+ };
+
+ unsigned int pval;
+ unsigned int val;
+ int ret;
+ u8 div;
+ u32 pcr_m;
+ u32 pcr_k;
+ u32 pcr_up;
+ u32 pcr_down;
+
+ /* DeSSC PLL reference clock is 25 MHz XTal. */
+ ret = regmap_write(ctx->regmap, 0x8226, 0x20);
+ if (ret)
+ return ret;
+
+ /* Prediv = 0 */
+ ret = regmap_write(ctx->regmap, 0x8227, 0x40);
+ if (ret)
+ return ret;
+
+ if (mode->clock < 44000) {
+ ret = regmap_write(ctx->regmap, 0x822f, 0x07);
+ div = 16;
+ } else if (mode->clock < 88000) {
+ ret = regmap_write(ctx->regmap, 0x822f, 0x06);
+ div = 8;
+ } else if (mode->clock < 176000) {
+ ret = regmap_write(ctx->regmap, 0x822f, 0x05);
+ div = 4;
+ } else {
+ ret = regmap_write(ctx->regmap, 0x822f, 0x04);
+ div = 2;
+ }
+
+ if (ret)
+ return ret;
+
+ ret = regmap_write(ctx->regmap, 0x822c, mode->clock < 22000 ? 0x01 : 0x00);
+ if (ret)
+ return ret;
+
+ pcr_m = (mode->clock * div) / 25;
+ pcr_k = pcr_m % 1000;
+ pcr_m /= 1000;
+
+ pcr_up = pcr_m + 1;
+ pcr_down = pcr_m - 1;
+
+ pcr_k <<= 14;
+
+ ret = regmap_write(ctx->regmap, 0xd008, 0x00);
+ if (ret < 0)
+ return ret;
+
+ /* 0xd026: pcr_m */
+ ret = regmap_write(ctx->regmap, 0xd026, (u8)pcr_m & 0x7f);
+ if (ret < 0)
+ return ret;
+
+ /* 0xd027 0xd028 0xd029: pcr_k */
+ ret = regmap_write(ctx->regmap, 0xd027, (pcr_k >> 16) & 0xff);
+ if (ret < 0)
+ return ret;
+
+ ret = regmap_write(ctx->regmap, 0xd028, (pcr_k >> 8) & 0xff);
+ if (ret < 0)
+ return ret;
+
+ ret = regmap_write(ctx->regmap, 0xd029, pcr_k & 0xff);
+ if (ret < 0)
+ return ret;
+
+ /* 0xd02d: pcr_m overflow limit setting */
+ ret = regmap_write(ctx->regmap, 0xd02d, pcr_up);
+ if (ret < 0)
+ return ret;
+
+ /* 0xd031: pcr_m underflow limit setting */
+ ret = regmap_write(ctx->regmap, 0xd031, pcr_down);
+ if (ret < 0)
+ return ret;
+
+ ret = regmap_multi_reg_write(ctx->regmap, lt9211c_dessc_pll_reset,
+ ARRAY_SIZE(lt9211c_dessc_pll_reset));
+ if (ret)
+ return ret;
+
+ ret = regmap_multi_reg_write(ctx->regmap, lt9211c_pcr_cali_seq,
+ ARRAY_SIZE(lt9211c_pcr_cali_seq));
+ if (ret)
+ return ret;
+
+ if (mode->clock < 44000)
+ val = 0x60;
+ else
+ val = 0x40;
+ ret = regmap_write(ctx->regmap, 0xd00c, val);
+ if (ret)
+ return ret;
+ ret = regmap_write(ctx->regmap, 0xd01b, 0x00);
+ if (ret)
+ return ret;
+ ret = regmap_write(ctx->regmap, 0xd01c, val);
+ if (ret)
+ return ret;
+
+ ret = regmap_multi_reg_write(ctx->regmap, lt9211c_pcr_reset_seq,
+ ARRAY_SIZE(lt9211c_pcr_reset_seq));
+ if (ret)
+ return ret;
+
+ /* PCR stability test takes seconds. */
+ ret = regmap_read_poll_timeout(ctx->regmap, 0xd087, pval,
+ ((pval & 0x18) == 0x18), 20000, 3000000);
+ if (ret)
+ dev_err(ctx->dev, "PCR unstable, ret=%i\n", ret);
+
+ ret = regmap_write(ctx->regmap, 0x8180, 0x51);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(ctx->regmap, 0x863f, 0x00);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(ctx->regmap, 0x863f, 0x01);
+ if (ret)
+ return ret;
+
+ ret = regmap_read_poll_timeout(ctx->regmap, 0x8640, pval,
+ ((pval & 0x01) == 0x01), 50000, 250000);
+ if (ret)
+ dev_err(ctx->dev, "Video check not stable, ret=%i\n", ret);
+
+ return ret;
+}
+
+static int lt9211c_configure_tx(struct lt9211 *ctx, bool jeida, bool bpp24,
+ bool de, const struct drm_display_mode *mode)
+{
+ const struct reg_sequence lt9211c_tx_phy_off_seq[] = {
+ { 0x8236, 0x00 },
+ { 0x8237, 0x00 },
+ { 0x8108, 0x6f },
+ { 0x8103, 0xbf },
+ };
+
+ const struct reg_sequence lt9211c_tx_phy_seq[] = {
+ { 0x8236, 0x03 },
+ { 0x8237, 0x44 },
+ { 0x8238, 0x14 },
+ { 0x8239, 0x31 },
+ { 0x823a, 0xc8 },
+ { 0x823b, 0x00 },
+ { 0x823c, 0x0f },
+ { 0x8246, 0x40 },
+ { 0x8247, 0x40 },
+ { 0x8248, 0x40 },
+ { 0x8249, 0x40 },
+ { 0x824a, 0x40 },
+ { 0x824b, 0x40 },
+ { 0x824c, 0x40 },
+ { 0x824d, 0x40 },
+ { 0x824e, 0x40 },
+ { 0x824f, 0x40 },
+ { 0x8250, 0x40 },
+ { 0x8251, 0x40 },
+ };
+
+ const struct reg_sequence lt9211c_tx_mltx_reset[] = {
+ { 0x8103, 0xbf },
+ { 0x8103, 0xff },
+ };
+
+ const struct reg_sequence lt9211c_tx_dig_seq[] = {
+ { 0x854a, 0x01 },
+ { 0x854b, 0x00 },
+ { 0x854c, 0x10 },
+ { 0x854d, 0x20 },
+ { 0x854e, 0x50 },
+ { 0x854f, 0x30 },
+ { 0x8550, 0x46 },
+ { 0x8551, 0x10 },
+ { 0x8552, 0x20 },
+ { 0x8553, 0x50 },
+ { 0x8554, 0x30 },
+ { 0x8555, 0x00 },
+ { 0x8556, 0x20 },
+
+ { 0x8568, 0x00 },
+ { 0x856e, 0x10 | (de ? BIT(6) : 0) },
+ { 0x856f, 0x81 | (jeida ? BIT(6) : 0) |
+ (ctx->lvds_dual_link ? BIT(4) : 0) |
+ (bpp24 ? BIT(2) : 0) },
+ };
+
+ const struct reg_sequence lt9211c_tx_ssc_seq[] = {
+ { 0x8234, 0x00 },
+ { 0x856e, 0x10 },
+ { 0x8181, 0x15 },
+ { 0x871e, 0x00 },
+ { 0x8717, 0x02 },
+ { 0x8718, 0x04 },
+ { 0x8719, 0xd4 },
+ { 0x871a, 0x00 },
+ { 0x871b, 0x12 },
+ { 0x871c, 0x00 },
+ { 0x871d, 0x24 },
+ { 0x871f, 0x1c },
+ { 0x8720, 0x00 },
+ { 0x8721, 0x00 },
+ { 0x871e, 0x02 },
+ };
+
+ const struct reg_sequence lt9211c_tx_pll_reset_seq[] = {
+ { 0x810c, 0xfe, 2000 },
+ { 0x810c, 0xff, 0 },
+ };
+
+ const struct reg_sequence lt9211c_tx_sw_reset_seq[] = {
+ { 0x8108, 0x6f, 2000 },
+ { 0x8108, 0x7f, 0 },
+ };
+
+ unsigned int pval;
+ int ret;
+ u32 phy_clk;
+ u8 pixclk_div;
+ u8 pre_div;
+ u8 div_set;
+ u8 sericlk_div;
+ u8 val;
+
+ dev_dbg(ctx->dev,
+ "dual_link=%d,even_odd_swap=%d,bpp24=%d,jeida=%d,de=%d\n",
+ ctx->lvds_dual_link, ctx->lvds_dual_link_even_odd_swap,
+ bpp24, jeida, de);
+
+ ret = regmap_multi_reg_write(ctx->regmap, lt9211c_tx_phy_off_seq,
+ ARRAY_SIZE(lt9211c_tx_phy_off_seq));
+ if (ret)
+ return ret;
+
+ ret = regmap_write_bits(ctx->regmap, 0x8530, 0xc0, 0x40);
+ if (ret)
+ return ret;
+
+ /* [7]0:txpll normal work; txpll ref clk sel pix clk */
+ ret = regmap_write(ctx->regmap, 0x8230, 0x00);
+ if (ret)
+ return ret;
+
+ if (ctx->lvds_dual_link)
+ phy_clk = (u32)(mode->clock * 7 / 2);
+ else
+ phy_clk = (u32)(mode->clock * 7);
+
+ /* 0x8231: prediv sel */
+ if (mode->clock < 40000) {
+ val = 0x28;
+ pre_div = 1;
+ } else if (mode->clock < 80000) {
+ val = 0x29;
+ pre_div = 2;
+ } else if (mode->clock < 160000) {
+ val = 0x2a;
+ pre_div = 4;
+ } else if (mode->clock < 320000) {
+ val = 0x2b;
+ pre_div = 8;
+ } else {
+ val = 0x2f;
+ pre_div = 16;
+ }
+ ret = regmap_write(ctx->regmap, 0x8231, val);
+ if (ret < 0)
+ return ret;
+
+ /* 0x8232: serickdiv sel */
+ if (phy_clk < 80000) {
+ val = 0x32;
+ sericlk_div = 16;
+ } else if (phy_clk < 160000) {
+ val = 0x22;
+ sericlk_div = 8;
+ } else if (phy_clk < 320000) {
+ val = 0x12;
+ sericlk_div = 4;
+ } else if (phy_clk < 640000) {
+ val = 0x02;
+ sericlk_div = 2;
+ } else {
+ val = 0x42;
+ sericlk_div = 1;
+ }
+ ret = regmap_write(ctx->regmap, 0x8232, val);
+ if (ret < 0)
+ return ret;
+
+ /*
+ * 0x8233: pix_mux sel & pix_div sel
+ * To avoid floating point operations, The pixclk_div is enlarged by 10 times
+ */
+ if (mode->clock > 150000) {
+ val = 0x04;
+ pixclk_div = 35;
+ } else {
+ pixclk_div =
+ (u8)((phy_clk * sericlk_div * 10) / (mode->clock * 7));
+ if (pixclk_div <= 10)
+ val = 0x00;
+ else if (pixclk_div <= 20)
+ val = 0x01;
+ else if (pixclk_div <= 40)
+ val = 0x02;
+ else
+ val = 0x03;
+ }
+ ret = regmap_write(ctx->regmap, 0x8233, val);
+ if (ret < 0)
+ return ret;
+
+ ret = regmap_write(ctx->regmap, 0x8234, 0x01);
+ if (ret < 0)
+ return ret;
+
+ /* 0x8235: div set */
+ div_set = (u8)(phy_clk * sericlk_div / mode->clock / pre_div);
+ ret = regmap_write(ctx->regmap, 0x8235, div_set);
+ if (ret < 0)
+ return ret;
+
+ ret = regmap_multi_reg_write(ctx->regmap, lt9211c_tx_ssc_seq,
+ ARRAY_SIZE(lt9211c_tx_ssc_seq));
+ if (ret)
+ return ret;
+
+ ret = regmap_multi_reg_write(ctx->regmap, lt9211c_tx_pll_reset_seq,
+ ARRAY_SIZE(lt9211c_tx_pll_reset_seq));
+ if (ret)
+ return ret;
+
+ ret = regmap_read_poll_timeout(ctx->regmap, 0x8739, pval, pval & 0x04,
+ 10000, 1000000);
+ if (ret) {
+ dev_err(ctx->dev, "TX PLL unstable, ret=%i\n", ret);
+ return ret;
+ }
+
+ ret = regmap_multi_reg_write(ctx->regmap, lt9211c_tx_phy_seq,
+ ARRAY_SIZE(lt9211c_tx_phy_seq));
+ if (ret)
+ return ret;
+
+ ret = regmap_multi_reg_write(ctx->regmap, lt9211c_tx_mltx_reset,
+ ARRAY_SIZE(lt9211c_tx_mltx_reset));
+ if (ret)
+ return ret;
+
+ ret = regmap_multi_reg_write(ctx->regmap, lt9211c_tx_dig_seq,
+ ARRAY_SIZE(lt9211c_tx_dig_seq));
+ if (ret)
+ return ret;
+
+ ret = regmap_multi_reg_write(ctx->regmap, lt9211c_tx_sw_reset_seq,
+ ARRAY_SIZE(lt9211c_tx_sw_reset_seq));
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+static void lt9211_delayed_work_func(struct work_struct *work)
+{
+ struct delayed_work *dw = to_delayed_work(work);
+ struct lt9211 *ctx = container_of(dw, struct lt9211, lt9211_dw);
+ const struct drm_display_mode *mode = &ctx->lt9211c_mode;
+ int ret;
+
+ /* wait for vblank irq to correctly configure bridge and autodetect stream */
+ ret = drm_crtc_wait_one_vblank(ctx->lt9211c_crtc);
+ if (ret) {
+ dev_err(ctx->dev, "LT9211C: wait for vblank failed: %d\n", ret);
+ return;
+ }
+
+ ret = lt9211c_configure_rx(ctx);
+ if (ret) {
+ dev_err(ctx->dev, "LT9211C: configure_rx failed: %d\n", ret);
+ return;
+ }
+
+ ret = lt9211c_autodetect_rx(ctx, mode);
+ if (ret) {
+ dev_err(ctx->dev, "LT9211C: autodetect_rx failed: %d\n", ret);
+ return;
+ }
+
+ ret = lt9211c_configure_timing(ctx, mode);
+ if (ret) {
+ dev_err(ctx->dev, "LT9211C: configure_timing failed: %d\n", ret);
+ return;
+ }
+
+ ret = lt9211c_configure_plls(ctx, mode);
+ if (ret) {
+ dev_err(ctx->dev, "LT9211C: configure_plls failed: %d\n", ret);
+ return;
+ }
+
+ ret = lt9211c_configure_tx(ctx, ctx->lt9211c_jeida, ctx->lt9211c_bpp24,
+ ctx->lt9211c_de_high, mode);
+ if (ret) {
+ dev_err(ctx->dev, "LT9211C: configure_tx failed: %d\n", ret);
+ return;
+ }
+}
+
static void lt9211_atomic_enable(struct drm_bridge *bridge,
struct drm_atomic_commit *state)
{
@@ -523,6 +1272,21 @@ static void lt9211_atomic_enable(struct drm_bridge *bridge,
if (ret)
return;
+ if (ctx->chip_data->chip_type == LT9211C) {
+ drm_mode_copy(&ctx->lt9211c_mode, mode);
+ ctx->lt9211c_bpp24 = lvds_format_24bpp;
+ ctx->lt9211c_jeida = lvds_format_jeida;
+ ctx->lt9211c_de_high = !!(bus_flags & DRM_BUS_FLAG_DE_HIGH);
+ ctx->lt9211c_crtc = crtc;
+
+ /*
+ * Needs a live DSI stream to auto-detect RX resolution/format;
+ * defer bridge configuration until after vblank.
+ */
+ queue_delayed_work(ctx->wq, &ctx->lt9211_dw, 0);
+ return;
+ }
+
ret = lt9211_system_init(ctx);
if (ret)
return;
@@ -557,6 +1321,9 @@ static void lt9211_atomic_disable(struct drm_bridge *bridge,
struct lt9211 *ctx = bridge_to_lt9211(bridge);
int ret;
+ if (ctx->chip_data->chip_type == LT9211C)
+ cancel_delayed_work_sync(&ctx->lt9211_dw);
+
/*
* Put the chip in reset, pull nRST line low,
* and assure lengthy 10ms reset low timing.
@@ -672,14 +1439,13 @@ static int lt9211_parse_dt(struct lt9211 *ctx)
static int lt9211_host_attach(struct lt9211 *ctx)
{
- const struct mipi_dsi_device_info info = {
- .type = "lt9211",
- .channel = 0,
- .node = NULL,
- };
struct device *dev = ctx->dev;
struct device_node *host_node;
struct device_node *endpoint;
+ struct mipi_dsi_device_info info = {
+ .channel = 0,
+ .node = NULL,
+ };
struct mipi_dsi_device *dsi;
struct mipi_dsi_host *host;
int dsi_lanes;
@@ -698,6 +1464,7 @@ static int lt9211_host_attach(struct lt9211 *ctx)
if (dsi_lanes < 0)
return dsi_lanes;
+ strscpy(info.type, ctx->chip_data->dsi_type, sizeof(info.type));
dsi = devm_mipi_dsi_device_register_full(dev, host, &info);
if (IS_ERR(dsi))
return dev_err_probe(dev, PTR_ERR(dsi),
@@ -707,10 +1474,7 @@ static int lt9211_host_attach(struct lt9211 *ctx)
dsi->lanes = dsi_lanes;
dsi->format = MIPI_DSI_FMT_RGB888;
- dsi->mode_flags = MIPI_DSI_MODE_VIDEO | MIPI_DSI_MODE_VIDEO_SYNC_PULSE |
- MIPI_DSI_MODE_VIDEO_HSE | MIPI_DSI_MODE_VIDEO_NO_HSA |
- MIPI_DSI_MODE_VIDEO_NO_HFP | MIPI_DSI_MODE_VIDEO_NO_HBP |
- MIPI_DSI_MODE_NO_EOT_PACKET;
+ dsi->mode_flags = ctx->chip_data->dsi_mode_flags;
ret = devm_mipi_dsi_attach(dev, dsi);
if (ret < 0) {
@@ -721,6 +1485,11 @@ static int lt9211_host_attach(struct lt9211 *ctx)
return 0;
}
+static void lt9211_destroy_wq(void *data)
+{
+ destroy_workqueue(data);
+}
+
static int lt9211_probe(struct i2c_client *client)
{
struct device *dev = &client->dev;
@@ -748,45 +1517,48 @@ static int lt9211_probe(struct i2c_client *client)
if (ret)
return ret;
- ctx->regmap = devm_regmap_init_i2c(client, <9211_regmap_config);
+ ctx->chip_data = i2c_get_match_data(client);
+ if (!ctx->chip_data)
+ return -ENODEV;
+
+ ctx->regmap = devm_regmap_init_i2c(client, ctx->chip_data->regmap_config);
if (IS_ERR(ctx->regmap))
return PTR_ERR(ctx->regmap);
- dev_set_drvdata(dev, ctx);
- i2c_set_clientdata(client, ctx);
+ if (ctx->chip_data->chip_type == LT9211C) {
+ ctx->wq = create_workqueue("lt9211_work");
+ if (!ctx->wq)
+ return -ENOMEM;
+ ret = devm_add_action_or_reset(dev, lt9211_destroy_wq, ctx->wq);
+ if (ret)
+ return ret;
+ INIT_DELAYED_WORK(&ctx->lt9211_dw, lt9211_delayed_work_func);
+ }
ctx->bridge.of_node = dev->of_node;
- drm_bridge_add(&ctx->bridge);
-
- ret = lt9211_host_attach(ctx);
+ ret = devm_drm_bridge_add(dev, &ctx->bridge);
if (ret)
- drm_bridge_remove(&ctx->bridge);
-
- return ret;
-}
-
-static void lt9211_remove(struct i2c_client *client)
-{
- struct lt9211 *ctx = i2c_get_clientdata(client);
+ return ret;
- drm_bridge_remove(&ctx->bridge);
+ return lt9211_host_attach(ctx);
}
static const struct i2c_device_id lt9211_id[] = {
- { .name = "lontium,lt9211" },
+ { .name = "lontium,lt9211", .driver_data = (kernel_ulong_t)<9211_chip_data },
+ { .name = "lontium,lt9211c", .driver_data = (kernel_ulong_t)<9211c_chip_data },
{ }
};
MODULE_DEVICE_TABLE(i2c, lt9211_id);
static const struct of_device_id lt9211_match_table[] = {
- { .compatible = "lontium,lt9211" },
+ { .compatible = "lontium,lt9211", .data = <9211_chip_data },
+ { .compatible = "lontium,lt9211c", .data = <9211c_chip_data },
{},
};
MODULE_DEVICE_TABLE(of, lt9211_match_table);
static struct i2c_driver lt9211_driver = {
.probe = lt9211_probe,
- .remove = lt9211_remove,
.id_table = lt9211_id,
.driver = {
.name = "lt9211",
--
2.34.1
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH v7 2/2] drm/bridge: add support for lontium lt9211c bridge
2026-08-22 17:01 ` [PATCH v7 2/2] drm/bridge: add support for lontium lt9211c bridge vishnu.saini
@ 2026-08-22 17:15 ` sashiko-bot
0 siblings, 0 replies; 4+ messages in thread
From: sashiko-bot @ 2026-08-22 17:15 UTC (permalink / raw)
To: vishnu.saini; +Cc: devicetree, dri-devel, robh, conor+dt
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
New issues:
- [High] Deferring LT9211C bridge configuration to an asynchronous workqueue breaks the DRM display pipeline enablement sequence and introduces race conditions.
- [High] Signed integer overflow in pixel clock calculations leads to undefined behavior.
- [High] Unchecked division by `mode->clock` can cause a kernel panic (divide-by-zero).
- [Medium] Chip ID detection succeeds for either hardware variant but ignores device tree mismatch, leading to incorrect hardware initialization.
Pre-existing issues:
- [High] Unbinding the device via sysfs frees the bridge context while it is still referenced by the DRM pipeline (Use-After-Free).
--
--- Patch [2]: [PATCH v7 2/2] drm/bridge: add support for lontium lt9211c bridge ---
commit 8d2ec0de9ed01158cbba906a0bfbb93f7fab90c7
Author: Yi Zhang <zhanyi@qti.qualcomm.com>
Subject: drm/bridge: add support for lontium lt9211c bridge
This commit extends the lontium-lt9211 driver to support DSI-to-LVDS
bridge configuration for both LT9211 and LT9211C variants. It detects
chip IDs, provides per-chip configuration, and implements an asynchronous
initialization sequence for the LT9211C.
> diff --git a/drivers/gpu/drm/bridge/lontium-lt9211.c b/drivers/gpu/drm/bridge/lontium-lt9211.c
> --- a/drivers/gpu/drm/bridge/lontium-lt9211.c
> +++ b/drivers/gpu/drm/bridge/lontium-lt9211.c
[ ... ]
> @@ -120,14 +209,23 @@ static int lt9211_read_chipid(struct lt9211 *ctx)
> return ret;
> }
>
> - /* Test for known Chip ID. */
> - if (chipid[0] != REG_CHIPID0_VALUE || chipid[1] != REG_CHIPID1_VALUE) {
> - dev_err(ctx->dev, "Unknown Chip ID: 0x%02x 0x%02x 0x%02x\n",
> - chipid[0], chipid[1], chipid[2]);
> - return -EINVAL;
> + /* Test for LT9211 Chip ID. */
> + if (chipid[0] == REG_CHIPID0_VALUE && chipid[1] == REG_CHIPID1_VALUE) {
> + dev_dbg(ctx->dev, "Detected LT9211 chip\n");
> + return 0;
> }
>
> - return 0;
> + /* Test for LT9211C Chip ID. */
> + if (chipid[0] == REG_CHIPID0_LT9211C_VALUE &&
> + chipid[1] == REG_CHIPID1_LT9211C_VALUE &&
> + chipid[2] == REG_CHIPID2_LT9211C_VALUE) {
> + dev_dbg(ctx->dev, "Detected LT9211C chip\n");
> + return 0;
> + }
[Severity: Medium]
Does this chip ID validation check the physically detected chip against the
expected configuration? Since it returns 0 for both variants without checking
ctx->chip_data->chip_type (which comes from the device tree compatible
string), could an incorrectly configured device tree allow the driver to
proceed with the wrong initialization sequence?
[ ... ]
> @@ -454,6 +552,657 @@ static int lt9211_configure_tx(struct lt9211 *ctx, bool jeida,
[ ... ]
> +static int lt9211c_configure_tx(struct lt9211 *ctx, bool jeida, bool bpp24,
> + bool de, const struct drm_display_mode *mode)
> +{
[ ... ]
> + if (ctx->lvds_dual_link)
> + phy_clk = (u32)(mode->clock * 7 / 2);
> + else
> + phy_clk = (u32)(mode->clock * 7);
[Severity: High]
Could this pixel clock calculation overflow? Since mode->clock is a signed
32-bit integer, multiplying it by 7 could overflow INT_MAX for high pixel
clocks (like a 4K resolution at ~594,000 kHz) before the result is cast to
u32.
Without a mode_valid or atomic_check callback to restrict the maximum
supported pixel clock, could this cause incorrect hardware initialization or
undefined behavior?
[ ... ]
> + /*
> + * 0x8233: pix_mux sel & pix_div sel
> + * To avoid floating point operations, The pixclk_div is enlarged by 10 times
> + */
> + if (mode->clock > 150000) {
> + val = 0x04;
> + pixclk_div = 35;
> + } else {
> + pixclk_div =
> + (u8)((phy_clk * sericlk_div * 10) / (mode->clock * 7));
[Severity: High]
Does this division need to ensure mode->clock is non-zero? If userspace
provides a malformed mode with a clock of 0, could this trigger a
divide-by-zero exception and kernel panic?
Given the driver doesn't implement mode_valid or atomic_check callbacks,
there appears to be no filtering of custom modes before this calculation,
or the similar division below at:
div_set = (u8)(phy_clk * sericlk_div / mode->clock / pre_div)
[ ... ]
> @@ -523,6 +1272,21 @@ static void lt9211_atomic_enable(struct drm_bridge *bridge,
> if (ret)
> return;
>
> + if (ctx->chip_data->chip_type == LT9211C) {
> + drm_mode_copy(&ctx->lt9211c_mode, mode);
> + ctx->lt9211c_bpp24 = lvds_format_24bpp;
> + ctx->lt9211c_jeida = lvds_format_jeida;
> + ctx->lt9211c_de_high = !!(bus_flags & DRM_BUS_FLAG_DE_HIGH);
> + ctx->lt9211c_crtc = crtc;
> +
> + /*
> + * Needs a live DSI stream to auto-detect RX resolution/format;
> + * defer bridge configuration until after vblank.
> + */
> + queue_delayed_work(ctx->wq, &ctx->lt9211_dw, 0);
> + return;
> + }
[Severity: High]
Does deferring the bridge configuration to an asynchronous workqueue break the
DRM atomic enablement sequence?
By returning immediately from atomic_enable(), downstream bridges like the
panel might be activated before the LT9211C has configured its RX, PLLs, and
TX output.
Additionally, if the CRTC relies on the bridge being active to generate
vblanks, could drm_crtc_wait_one_vblank() in lt9211_delayed_work_func()
cause a deadlock? If the pipeline is disabled while the work is running,
could cancel_delayed_work_sync() block indefinitely or cause a
use-after-free?
[ ... ]
> @@ -748,45 +1517,48 @@ static int lt9211_probe(struct i2c_client *client)
[ ... ]
> - ctx->bridge.of_node = dev->of_node;
> - drm_bridge_add(&ctx->bridge);
> -
> - ret = lt9211_host_attach(ctx);
> + ctx->bridge.of_node = dev->of_node;
> + ret = devm_drm_bridge_add(dev, &ctx->bridge);
> if (ret)
> - drm_bridge_remove(&ctx->bridge);
> -
> - return ret;
> -}
> -
> -static void lt9211_remove(struct i2c_client *client)
> -{
> - struct lt9211 *ctx = i2c_get_clientdata(client);
> + return ret;
>
> - drm_bridge_remove(&ctx->bridge);
> + return lt9211_host_attach(ctx);
> }
[Severity: High]
This is a pre-existing issue, but does tying the bridge context to the device
lifetime (via devm_drm_bridge_alloc and devm_drm_bridge_add) instead of the
DRM object lifetime leave the module vulnerable?
If the device is unbound via sysfs while the display is active, the devres
cleanup frees the context memory. Since the DRM core doesn't synchronously
tear down active displays on bridge removal, would subsequent calls by the DRM
core to functions like lt9211_atomic_disable() on the freed pointer result in
a use-after-free?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260822-add-lt9211c-bridge-v7-0-7dcf567d57c4@oss.qualcomm.com?part=2
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-08-22 17:15 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-22 17:01 [PATCH v7 0/2] This series adds LT9211C bridge driver by extending LT9211 vishnu.saini
2026-08-22 17:01 ` [PATCH v7 1/2] dt-bindings: display: bridge: lontium,lt9211: Add lt9211c support vishnu.saini
2026-08-22 17:01 ` [PATCH v7 2/2] drm/bridge: add support for lontium lt9211c bridge vishnu.saini
2026-08-22 17:15 ` sashiko-bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox