]> git.baikalelectronics.ru Git - kernel.git/commitdiff
drm/bridge: lt9211: Add Lontium LT9211 bridge driver
authorMarek Vasut <marex@denx.de>
Tue, 19 Apr 2022 14:39:58 +0000 (16:39 +0200)
committerRobert Foss <robert.foss@linaro.org>
Tue, 19 Apr 2022 14:47:38 +0000 (16:47 +0200)
Add driver for Lontium LT9211 Single/Dual-Link DSI/LVDS or Single DPI to
Single-link/Dual-Link DSI/LVDS or Single DPI bridge. This chip is highly
capable at converting formats, but sadly it is also highly undocumented.

This driver is written without any documentation from Lontium and based
only on shreds of information available in various obscure example codes,
hence long runs of unknown register patches and lengthy delays in various
places. Whichever register meaning could be divined from its behavior has
at least a comment around it.

Currently the only mode tested is Single-link DSI to Single-link LVDS.
Dual-link LVDS might work as well, the register programming is in place,
but is untested.

Reviewed-by: Robert Foss <robert.foss@linaro.org>
Signed-off-by: Marek Vasut <marex@denx.de>
Cc: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Cc: Lucas Stach <l.stach@pengutronix.de>
Cc: Maxime Ripard <maxime@cerno.tech>
Cc: Robert Foss <robert.foss@linaro.org>
Cc: Sam Ravnborg <sam@ravnborg.org>
Cc: Thomas Zimmermann <tzimmermann@suse.de>
To: dri-devel@lists.freedesktop.org
Signed-off-by: Robert Foss <robert.foss@linaro.org>
Link: https://patchwork.freedesktop.org/patch/msgid/20220419143958.94873-2-marex@denx.de
drivers/gpu/drm/bridge/Kconfig
drivers/gpu/drm/bridge/Makefile
drivers/gpu/drm/bridge/lontium-lt9211.c [new file with mode: 0644]

index 7ac920a57d263e8de9609ed8ff018bda5c87da4a..c08ccb4b332b0dd9cc0554a405169f2c9df79938 100644 (file)
@@ -100,6 +100,19 @@ config DRM_LONTIUM_LT8912B
          Say M here if you want to support this hardware as a module.
          The module will be named "lontium-lt8912b".
 
+config DRM_LONTIUM_LT9211
+       tristate "Lontium LT9211 DSI/LVDS/DPI bridge"
+       depends on OF
+       select DRM_PANEL_BRIDGE
+       select DRM_KMS_HELPER
+       select DRM_MIPI_DSI
+       select REGMAP_I2C
+       help
+         Driver for Lontium LT9211 Single/Dual-Link DSI/LVDS or Single DPI
+         input to Single-link/Dual-Link DSI/LVDS or Single DPI output bridge
+         chip.
+         Please say Y if you have such hardware.
+
 config DRM_LONTIUM_LT9611
        tristate "Lontium LT9611 DSI/HDMI bridge"
        select SND_SOC_HDMI_CODEC if SND_SOC
index 425844c30495342497d8fd0998aa45b88280e669..b0edf2022fa07bc13ee89d98ef703451d04b08f8 100644 (file)
@@ -6,6 +6,7 @@ obj-$(CONFIG_DRM_CROS_EC_ANX7688) += cros-ec-anx7688.o
 obj-$(CONFIG_DRM_DISPLAY_CONNECTOR) += display-connector.o
 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_LT9611UXC) += lontium-lt9611uxc.o
 obj-$(CONFIG_DRM_LVDS_CODEC) += lvds-codec.o
diff --git a/drivers/gpu/drm/bridge/lontium-lt9211.c b/drivers/gpu/drm/bridge/lontium-lt9211.c
new file mode 100644 (file)
index 0000000..e92821f
--- /dev/null
@@ -0,0 +1,802 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Lontium LT9211 bridge driver
+ *
+ * LT9211 is capable of converting:
+ *   2xDSI/2xLVDS/1xDPI -> 2xDSI/2xLVDS/1xDPI
+ * Currently supported is:
+ *   1xDSI -> 1xLVDS
+ *
+ * Copyright (C) 2022 Marek Vasut <marex@denx.de>
+ */
+
+#include <linux/bits.h>
+#include <linux/clk.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/module.h>
+#include <linux/of_device.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_mipi_dsi.h>
+#include <drm/drm_of.h>
+#include <drm/drm_panel.h>
+#include <drm/drm_print.h>
+#include <drm/drm_probe_helper.h>
+
+#define REG_PAGE_CONTROL                       0xff
+#define REG_CHIPID0                            0x8100
+#define REG_CHIPID0_VALUE                      0x18
+#define REG_CHIPID1                            0x8101
+#define REG_CHIPID1_VALUE                      0x01
+#define REG_CHIPID2                            0x8102
+#define REG_CHIPID2_VALUE                      0xe3
+
+#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)
+
+struct lt9211 {
+       struct drm_bridge               bridge;
+       struct device                   *dev;
+       struct regmap                   *regmap;
+       struct mipi_dsi_device          *dsi;
+       struct drm_bridge               *panel_bridge;
+       struct gpio_desc                *reset_gpio;
+       struct regulator                *vccio;
+       bool                            lvds_dual_link;
+       bool                            lvds_dual_link_even_odd_swap;
+};
+
+static const struct regmap_range lt9211_rw_ranges[] = {
+       regmap_reg_range(0xff, 0xff),
+       regmap_reg_range(0x8100, 0x816b),
+       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 lt9211_rw_table = {
+       .yes_ranges = lt9211_rw_ranges,
+       .n_yes_ranges = ARRAY_SIZE(lt9211_rw_ranges),
+};
+
+static const struct regmap_range_cfg lt9211_range = {
+       .name = "lt9211",
+       .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 lt9211_regmap_config = {
+       .reg_bits = 8,
+       .val_bits = 8,
+       .rd_table = &lt9211_rw_table,
+       .wr_table = &lt9211_rw_table,
+       .volatile_table = &lt9211_rw_table,
+       .ranges = &lt9211_range,
+       .num_ranges = 1,
+       .cache_type = REGCACHE_RBTREE,
+       .max_register = 0xda00,
+};
+
+static struct lt9211 *bridge_to_lt9211(struct drm_bridge *bridge)
+{
+       return container_of(bridge, struct lt9211, bridge);
+}
+
+static int lt9211_attach(struct drm_bridge *bridge,
+                        enum drm_bridge_attach_flags flags)
+{
+       struct lt9211 *ctx = bridge_to_lt9211(bridge);
+
+       return drm_bridge_attach(bridge->encoder, ctx->panel_bridge,
+                                &ctx->bridge, flags);
+}
+
+static int lt9211_read_chipid(struct lt9211 *ctx)
+{
+       u8 chipid[3];
+       int ret;
+
+       /* Read Chip ID registers and verify the chip can communicate. */
+       ret = regmap_bulk_read(ctx->regmap, REG_CHIPID0, chipid, 3);
+       if (ret < 0) {
+               dev_err(ctx->dev, "Failed to read Chip ID: %d\n", ret);
+               return ret;
+       }
+
+       /* Test for known Chip ID. */
+       if (chipid[0] != REG_CHIPID0_VALUE || chipid[1] != REG_CHIPID1_VALUE ||
+           chipid[2] != REG_CHIPID2_VALUE) {
+               dev_err(ctx->dev, "Unknown Chip ID: 0x%02x 0x%02x 0x%02x\n",
+                       chipid[0], chipid[1], chipid[2]);
+               return -EINVAL;
+       }
+
+       return 0;
+}
+
+static int lt9211_system_init(struct lt9211 *ctx)
+{
+       const struct reg_sequence lt9211_system_init_seq[] = {
+               { 0x8201, 0x18 },
+               { 0x8606, 0x61 },
+               { 0x8607, 0xa8 },
+               { 0x8714, 0x08 },
+               { 0x8715, 0x00 },
+               { 0x8718, 0x0f },
+               { 0x8722, 0x08 },
+               { 0x8723, 0x00 },
+               { 0x8726, 0x0f },
+               { 0x810b, 0xfe },
+       };
+
+       return regmap_multi_reg_write(ctx->regmap, lt9211_system_init_seq,
+                                     ARRAY_SIZE(lt9211_system_init_seq));
+}
+
+static int lt9211_configure_rx(struct lt9211 *ctx)
+{
+       const struct reg_sequence lt9211_rx_phy_seq[] = {
+               { 0x8202, 0x44 },
+               { 0x8204, 0xa0 },
+               { 0x8205, 0x22 },
+               { 0x8207, 0x9f },
+               { 0x8208, 0xfc },
+               /* ORR with 0xf8 here to enable DSI DN/DP swap. */
+               { 0x8209, 0x01 },
+               { 0x8217, 0x0c },
+               { 0x8633, 0x1b },
+       };
+
+       const struct reg_sequence lt9211_rx_cal_reset_seq[] = {
+               { 0x8120, 0x7f },
+               { 0x8120, 0xff },
+       };
+
+       const struct reg_sequence lt9211_rx_dig_seq[] = {
+               { 0x8630, 0x85 },
+               /* 0x8588: BIT 6 set = MIPI-RX, BIT 4 unset = LVDS-TX */
+               { 0x8588, 0x40 },
+               { 0x85ff, 0xd0 },
+               { REG_DSI_LANE, REG_DSI_LANE_COUNT(ctx->dsi->lanes) },
+               { 0xd002, 0x05 },
+       };
+
+       const struct reg_sequence lt9211_rx_div_reset_seq[] = {
+               { 0x810a, 0xc0 },
+               { 0x8120, 0xbf },
+       };
+
+       const struct reg_sequence lt9211_rx_div_clear_seq[] = {
+               { 0x810a, 0xc1 },
+               { 0x8120, 0xff },
+       };
+
+       int ret;
+
+       ret = regmap_multi_reg_write(ctx->regmap, lt9211_rx_phy_seq,
+                                    ARRAY_SIZE(lt9211_rx_phy_seq));
+       if (ret)
+               return ret;
+
+       ret = regmap_multi_reg_write(ctx->regmap, lt9211_rx_cal_reset_seq,
+                                    ARRAY_SIZE(lt9211_rx_cal_reset_seq));
+       if (ret)
+               return ret;
+
+       ret = regmap_multi_reg_write(ctx->regmap, lt9211_rx_dig_seq,
+                                    ARRAY_SIZE(lt9211_rx_dig_seq));
+       if (ret)
+               return ret;
+
+       ret = regmap_multi_reg_write(ctx->regmap, lt9211_rx_div_reset_seq,
+                                    ARRAY_SIZE(lt9211_rx_div_reset_seq));
+       if (ret)
+               return ret;
+
+       usleep_range(10000, 15000);
+
+       return regmap_multi_reg_write(ctx->regmap, lt9211_rx_div_clear_seq,
+                                     ARRAY_SIZE(lt9211_rx_div_clear_seq));
+}
+
+static int lt9211_autodetect_rx(struct lt9211 *ctx,
+                               const struct drm_display_mode *mode)
+{
+       u16 width, height;
+       u32 byteclk;
+       u8 buf[5];
+       u8 format;
+       u8 bc[3];
+       int ret;
+
+       /* Measure ByteClock frequency. */
+       ret = regmap_write(ctx->regmap, 0x8600, 0x01);
+       if (ret)
+               return ret;
+
+       /* Give the chip time to lock onto RX stream. */
+       msleep(100);
+
+       /* Read the ByteClock frequency from the chip. */
+       ret = regmap_bulk_read(ctx->regmap, 0x8608, bc, sizeof(bc));
+       if (ret)
+               return ret;
+
+       /* RX ByteClock in kHz */
+       byteclk = ((bc[0] & 0xf) << 16) | (bc[1] << 8) | bc[2];
+
+       /* 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 pixel format 0x%01x\n",
+                       format);
+               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 byteclock=%d kHz\n",
+               width, height, format, byteclk);
+
+       return 0;
+}
+
+static int lt9211_configure_timing(struct lt9211 *ctx,
+                                  const struct drm_display_mode *mode)
+{
+       const struct reg_sequence lt9211_timing[] = {
+               { 0xd00d, (mode->vtotal >> 8) & 0xff },
+               { 0xd00e, mode->vtotal & 0xff },
+               { 0xd00f, (mode->vdisplay >> 8) & 0xff },
+               { 0xd010, mode->vdisplay & 0xff },
+               { 0xd011, (mode->htotal >> 8) & 0xff },
+               { 0xd012, mode->htotal & 0xff },
+               { 0xd013, (mode->hdisplay >> 8) & 0xff },
+               { 0xd014, mode->hdisplay & 0xff },
+               { 0xd015, (mode->vsync_end - mode->vsync_start) & 0xff },
+               { 0xd016, (mode->hsync_end - mode->hsync_start) & 0xff },
+               { 0xd017, ((mode->vsync_start - mode->vdisplay) >> 8) & 0xff },
+               { 0xd018, (mode->vsync_start - mode->vdisplay) & 0xff },
+               { 0xd019, ((mode->hsync_start - mode->hdisplay) >> 8) & 0xff },
+               { 0xd01a, (mode->hsync_start - mode->hdisplay) & 0xff },
+       };
+
+       return regmap_multi_reg_write(ctx->regmap, lt9211_timing,
+                                     ARRAY_SIZE(lt9211_timing));
+}
+
+static int lt9211_configure_plls(struct lt9211 *ctx,
+                                const struct drm_display_mode *mode)
+{
+       const struct reg_sequence lt9211_pcr_seq[] = {
+               { 0xd026, 0x17 },
+               { 0xd027, 0xc3 },
+               { 0xd02d, 0x30 },
+               { 0xd031, 0x10 },
+               { 0xd023, 0x20 },
+               { 0xd038, 0x02 },
+               { 0xd039, 0x10 },
+               { 0xd03a, 0x20 },
+               { 0xd03b, 0x60 },
+               { 0xd03f, 0x04 },
+               { 0xd040, 0x08 },
+               { 0xd041, 0x10 },
+               { 0x810b, 0xee },
+               { 0x810b, 0xfe },
+       };
+
+       unsigned int pval;
+       int ret;
+
+       /* DeSSC PLL reference clock is 25 MHz XTal. */
+       ret = regmap_write(ctx->regmap, 0x822d, 0x48);
+       if (ret)
+               return ret;
+
+       if (mode->clock < 44000) {
+               ret = regmap_write(ctx->regmap, 0x8235, 0x83);
+       } else if (mode->clock < 88000) {
+               ret = regmap_write(ctx->regmap, 0x8235, 0x82);
+       } else if (mode->clock < 176000) {
+               ret = regmap_write(ctx->regmap, 0x8235, 0x81);
+       } else {
+               dev_err(ctx->dev,
+                       "Unsupported mode clock (%d kHz) above 176 MHz.\n",
+                       mode->clock);
+               return -EINVAL;
+       }
+
+       if (ret)
+               return ret;
+
+       /* Wait for the DeSSC PLL to stabilize. */
+       msleep(100);
+
+       ret = regmap_multi_reg_write(ctx->regmap, lt9211_pcr_seq,
+                                    ARRAY_SIZE(lt9211_pcr_seq));
+       if (ret)
+               return ret;
+
+       /* PCR stability test takes seconds. */
+       ret = regmap_read_poll_timeout(ctx->regmap, 0xd087, pval, pval & 0x8,
+                                      20000, 10000000);
+       if (ret)
+               dev_err(ctx->dev, "PCR unstable, ret=%i\n", ret);
+
+       return ret;
+}
+
+static int lt9211_configure_tx(struct lt9211 *ctx, bool jeida,
+                              bool bpp24, bool de)
+{
+       const struct reg_sequence system_lt9211_tx_phy_seq[] = {
+               /* DPI output disable */
+               { 0x8262, 0x00 },
+               /* BIT(7) is LVDS dual-port */
+               { 0x823b, 0x38 | (ctx->lvds_dual_link ? BIT(7) : 0) },
+               { 0x823e, 0x92 },
+               { 0x823f, 0x48 },
+               { 0x8240, 0x31 },
+               { 0x8243, 0x80 },
+               { 0x8244, 0x00 },
+               { 0x8245, 0x00 },
+               { 0x8249, 0x00 },
+               { 0x824a, 0x01 },
+               { 0x824e, 0x00 },
+               { 0x824f, 0x00 },
+               { 0x8250, 0x00 },
+               { 0x8253, 0x00 },
+               { 0x8254, 0x01 },
+               /* LVDS channel order, Odd:Even 0x10..A:B, 0x40..B:A */
+               { 0x8646, ctx->lvds_dual_link_even_odd_swap ? 0x40 : 0x10 },
+               { 0x8120, 0x7b },
+               { 0x816b, 0xff },
+       };
+
+       const struct reg_sequence system_lt9211_tx_dig_seq[] = {
+               { 0x8559, 0x40 | (jeida ? BIT(7) : 0) |
+                         (de ? BIT(5) : 0) | (bpp24 ? BIT(4) : 0) },
+               { 0x855a, 0xaa },
+               { 0x855b, 0xaa },
+               { 0x855c, ctx->lvds_dual_link ? BIT(0) : 0 },
+               { 0x85a1, 0x77 },
+               { 0x8640, 0x40 },
+               { 0x8641, 0x34 },
+               { 0x8642, 0x10 },
+               { 0x8643, 0x23 },
+               { 0x8644, 0x41 },
+               { 0x8645, 0x02 },
+       };
+
+       const struct reg_sequence system_lt9211_tx_pll_seq[] = {
+               /* TX PLL power down */
+               { 0x8236, 0x01 },
+               { 0x8237, ctx->lvds_dual_link ? 0x2a : 0x29 },
+               { 0x8238, 0x06 },
+               { 0x8239, 0x30 },
+               { 0x823a, 0x8e },
+               { 0x8737, 0x14 },
+               { 0x8713, 0x00 },
+               { 0x8713, 0x80 },
+       };
+
+       unsigned int pval;
+       int ret;
+
+       ret = regmap_multi_reg_write(ctx->regmap, system_lt9211_tx_phy_seq,
+                                    ARRAY_SIZE(system_lt9211_tx_phy_seq));
+       if (ret)
+               return ret;
+
+       ret = regmap_multi_reg_write(ctx->regmap, system_lt9211_tx_dig_seq,
+                                    ARRAY_SIZE(system_lt9211_tx_dig_seq));
+       if (ret)
+               return ret;
+
+       ret = regmap_multi_reg_write(ctx->regmap, system_lt9211_tx_pll_seq,
+                                    ARRAY_SIZE(system_lt9211_tx_pll_seq));
+       if (ret)
+               return ret;
+
+       ret = regmap_read_poll_timeout(ctx->regmap, 0x871f, pval, pval & 0x80,
+                                      10000, 1000000);
+       if (ret) {
+               dev_err(ctx->dev, "TX PLL unstable, ret=%i\n", ret);
+               return ret;
+       }
+
+       ret = regmap_read_poll_timeout(ctx->regmap, 0x8720, pval, pval & 0x80,
+                                      10000, 1000000);
+       if (ret) {
+               dev_err(ctx->dev, "TX PLL unstable, ret=%i\n", ret);
+               return ret;
+       }
+
+       return 0;
+}
+
+static void lt9211_atomic_enable(struct drm_bridge *bridge,
+                                struct drm_bridge_state *old_bridge_state)
+{
+       struct lt9211 *ctx = bridge_to_lt9211(bridge);
+       struct drm_atomic_state *state = old_bridge_state->base.state;
+       const struct drm_bridge_state *bridge_state;
+       const struct drm_crtc_state *crtc_state;
+       const struct drm_display_mode *mode;
+       struct drm_connector *connector;
+       struct drm_crtc *crtc;
+       bool lvds_format_24bpp;
+       bool lvds_format_jeida;
+       u32 bus_flags;
+       int ret;
+
+       ret = regulator_enable(ctx->vccio);
+       if (ret) {
+               dev_err(ctx->dev, "Failed to enable vccio: %d\n", ret);
+               return;
+       }
+
+       /* Deassert reset */
+       gpiod_set_value(ctx->reset_gpio, 1);
+       usleep_range(20000, 21000);     /* Very long post-reset delay. */
+
+       /* Get the LVDS format from the bridge state. */
+       bridge_state = drm_atomic_get_new_bridge_state(state, bridge);
+       bus_flags = bridge_state->output_bus_cfg.flags;
+
+       switch (bridge_state->output_bus_cfg.format) {
+       case MEDIA_BUS_FMT_RGB666_1X7X3_SPWG:
+               lvds_format_24bpp = false;
+               lvds_format_jeida = true;
+               break;
+       case MEDIA_BUS_FMT_RGB888_1X7X4_JEIDA:
+               lvds_format_24bpp = true;
+               lvds_format_jeida = true;
+               break;
+       case MEDIA_BUS_FMT_RGB888_1X7X4_SPWG:
+               lvds_format_24bpp = true;
+               lvds_format_jeida = false;
+               break;
+       default:
+               /*
+                * Some bridges still don't set the correct
+                * LVDS bus pixel format, use SPWG24 default
+                * format until those are fixed.
+                */
+               lvds_format_24bpp = true;
+               lvds_format_jeida = false;
+               dev_warn(ctx->dev,
+                        "Unsupported LVDS bus format 0x%04x, please check output bridge driver. Falling back to SPWG24.\n",
+                        bridge_state->output_bus_cfg.format);
+               break;
+       }
+
+       /*
+        * Retrieve the CRTC adjusted mode. This requires a little dance to go
+        * from the bridge to the encoder, to the connector and to the CRTC.
+        */
+       connector = drm_atomic_get_new_connector_for_encoder(state,
+                                                            bridge->encoder);
+       crtc = drm_atomic_get_new_connector_state(state, connector)->crtc;
+       crtc_state = drm_atomic_get_new_crtc_state(state, crtc);
+       mode = &crtc_state->adjusted_mode;
+
+       ret = lt9211_read_chipid(ctx);
+       if (ret)
+               return;
+
+       ret = lt9211_system_init(ctx);
+       if (ret)
+               return;
+
+       ret = lt9211_configure_rx(ctx);
+       if (ret)
+               return;
+
+       ret = lt9211_autodetect_rx(ctx, mode);
+       if (ret)
+               return;
+
+       ret = lt9211_configure_timing(ctx, mode);
+       if (ret)
+               return;
+
+       ret = lt9211_configure_plls(ctx, mode);
+       if (ret)
+               return;
+
+       ret = lt9211_configure_tx(ctx, lvds_format_jeida, lvds_format_24bpp,
+                                 bus_flags & DRM_BUS_FLAG_DE_HIGH);
+       if (ret)
+               return;
+
+       dev_dbg(ctx->dev, "LT9211 enabled.\n");
+}
+
+static void lt9211_atomic_disable(struct drm_bridge *bridge,
+                                 struct drm_bridge_state *old_bridge_state)
+{
+       struct lt9211 *ctx = bridge_to_lt9211(bridge);
+       int ret;
+
+       /*
+        * Put the chip in reset, pull nRST line low,
+        * and assure lengthy 10ms reset low timing.
+        */
+       gpiod_set_value(ctx->reset_gpio, 0);
+       usleep_range(10000, 11000);     /* Very long reset duration. */
+
+       ret = regulator_disable(ctx->vccio);
+       if (ret)
+               dev_err(ctx->dev, "Failed to disable vccio: %d\n", ret);
+
+       regcache_mark_dirty(ctx->regmap);
+}
+
+static enum drm_mode_status
+lt9211_mode_valid(struct drm_bridge *bridge,
+                 const struct drm_display_info *info,
+                 const struct drm_display_mode *mode)
+{
+       /* LVDS output clock range 25..176 MHz */
+       if (mode->clock < 25000)
+               return MODE_CLOCK_LOW;
+       if (mode->clock > 176000)
+               return MODE_CLOCK_HIGH;
+
+       return MODE_OK;
+}
+
+#define MAX_INPUT_SEL_FORMATS  1
+
+static u32 *
+lt9211_atomic_get_input_bus_fmts(struct drm_bridge *bridge,
+                                struct drm_bridge_state *bridge_state,
+                                struct drm_crtc_state *crtc_state,
+                                struct drm_connector_state *conn_state,
+                                u32 output_fmt,
+                                unsigned int *num_input_fmts)
+{
+       u32 *input_fmts;
+
+       *num_input_fmts = 0;
+
+       input_fmts = kcalloc(MAX_INPUT_SEL_FORMATS, sizeof(*input_fmts),
+                            GFP_KERNEL);
+       if (!input_fmts)
+               return NULL;
+
+       /* This is the DSI-end bus format */
+       input_fmts[0] = MEDIA_BUS_FMT_RGB888_1X24;
+       *num_input_fmts = 1;
+
+       return input_fmts;
+}
+
+static const struct drm_bridge_funcs lt9211_funcs = {
+       .attach                 = lt9211_attach,
+       .mode_valid             = lt9211_mode_valid,
+       .atomic_enable          = lt9211_atomic_enable,
+       .atomic_disable         = lt9211_atomic_disable,
+       .atomic_duplicate_state = drm_atomic_helper_bridge_duplicate_state,
+       .atomic_destroy_state   = drm_atomic_helper_bridge_destroy_state,
+       .atomic_get_input_bus_fmts = lt9211_atomic_get_input_bus_fmts,
+       .atomic_reset           = drm_atomic_helper_bridge_reset,
+};
+
+static int lt9211_parse_dt(struct lt9211 *ctx)
+{
+       struct device_node *port2, *port3;
+       struct drm_bridge *panel_bridge;
+       struct device *dev = ctx->dev;
+       struct drm_panel *panel;
+       int dual_link;
+       int ret;
+
+       ctx->vccio = devm_regulator_get(dev, "vccio");
+       if (IS_ERR(ctx->vccio))
+               return dev_err_probe(dev, PTR_ERR(ctx->vccio),
+                                    "Failed to get supply 'vccio'\n");
+
+       ctx->lvds_dual_link = false;
+       ctx->lvds_dual_link_even_odd_swap = false;
+
+       port2 = of_graph_get_port_by_id(dev->of_node, 2);
+       port3 = of_graph_get_port_by_id(dev->of_node, 3);
+       dual_link = drm_of_lvds_get_dual_link_pixel_order(port2, port3);
+       of_node_put(port2);
+       of_node_put(port3);
+
+       if (dual_link == DRM_LVDS_DUAL_LINK_ODD_EVEN_PIXELS) {
+               ctx->lvds_dual_link = true;
+               /* Odd pixels to LVDS Channel A, even pixels to B */
+               ctx->lvds_dual_link_even_odd_swap = false;
+       } else if (dual_link == DRM_LVDS_DUAL_LINK_EVEN_ODD_PIXELS) {
+               ctx->lvds_dual_link = true;
+               /* Even pixels to LVDS Channel A, odd pixels to B */
+               ctx->lvds_dual_link_even_odd_swap = true;
+       }
+
+       ret = drm_of_find_panel_or_bridge(dev->of_node, 2, 0, &panel, &panel_bridge);
+       if (ret < 0)
+               return ret;
+       if (panel) {
+               panel_bridge = devm_drm_panel_bridge_add(dev, panel);
+               if (IS_ERR(panel_bridge))
+                       return PTR_ERR(panel_bridge);
+       }
+
+       ctx->panel_bridge = panel_bridge;
+
+       return 0;
+}
+
+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 *dsi;
+       struct mipi_dsi_host *host;
+       int dsi_lanes;
+       int ret;
+
+       endpoint = of_graph_get_endpoint_by_regs(dev->of_node, 0, -1);
+       dsi_lanes = of_property_count_u32_elems(endpoint, "data-lanes");
+       host_node = of_graph_get_remote_port_parent(endpoint);
+       host = of_find_mipi_dsi_host_by_node(host_node);
+       of_node_put(host_node);
+       of_node_put(endpoint);
+
+       if (!host)
+               return -EPROBE_DEFER;
+
+       if (dsi_lanes < 0 || dsi_lanes > 4)
+               return -EINVAL;
+
+       dsi = devm_mipi_dsi_device_register_full(dev, host, &info);
+       if (IS_ERR(dsi))
+               return dev_err_probe(dev, PTR_ERR(dsi),
+                                    "failed to create dsi device\n");
+
+       ctx->dsi = dsi;
+
+       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;
+
+       ret = devm_mipi_dsi_attach(dev, dsi);
+       if (ret < 0) {
+               dev_err(dev, "failed to attach dsi to host: %d\n", ret);
+               return ret;
+       }
+
+       return 0;
+}
+
+static int lt9211_probe(struct i2c_client *client,
+                       const struct i2c_device_id *id)
+{
+       struct device *dev = &client->dev;
+       struct lt9211 *ctx;
+       int ret;
+
+       ctx = devm_kzalloc(dev, sizeof(*ctx), GFP_KERNEL);
+       if (!ctx)
+               return -ENOMEM;
+
+       ctx->dev = dev;
+
+       /*
+        * Put the chip in reset, pull nRST line low,
+        * and assure lengthy 10ms reset low timing.
+        */
+       ctx->reset_gpio = devm_gpiod_get_optional(ctx->dev, "reset",
+                                                 GPIOD_OUT_LOW);
+       if (IS_ERR(ctx->reset_gpio))
+               return PTR_ERR(ctx->reset_gpio);
+
+       usleep_range(10000, 11000);     /* Very long reset duration. */
+
+       ret = lt9211_parse_dt(ctx);
+       if (ret)
+               return ret;
+
+       ctx->regmap = devm_regmap_init_i2c(client, &lt9211_regmap_config);
+       if (IS_ERR(ctx->regmap))
+               return PTR_ERR(ctx->regmap);
+
+       dev_set_drvdata(dev, ctx);
+       i2c_set_clientdata(client, ctx);
+
+       ctx->bridge.funcs = &lt9211_funcs;
+       ctx->bridge.of_node = dev->of_node;
+       drm_bridge_add(&ctx->bridge);
+
+       ret = lt9211_host_attach(ctx);
+       if (ret)
+               drm_bridge_remove(&ctx->bridge);
+
+       return ret;
+}
+
+static int lt9211_remove(struct i2c_client *client)
+{
+       struct lt9211 *ctx = i2c_get_clientdata(client);
+
+       drm_bridge_remove(&ctx->bridge);
+
+       return 0;
+}
+
+static struct i2c_device_id lt9211_id[] = {
+       { "lontium,lt9211" },
+       {},
+};
+MODULE_DEVICE_TABLE(i2c, lt9211_id);
+
+static const struct of_device_id lt9211_match_table[] = {
+       { .compatible = "lontium,lt9211" },
+       {},
+};
+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",
+               .of_match_table = lt9211_match_table,
+       },
+};
+module_i2c_driver(lt9211_driver);
+
+MODULE_AUTHOR("Marek Vasut <marex@denx.de>");
+MODULE_DESCRIPTION("Lontium LT9211 DSI/LVDS/DPI bridge driver");
+MODULE_LICENSE("GPL");