Add support for the LVDS Display Bridge (LDB) found on i.MX8.

When attached, the bridge driver looks for panels connected to one of
its two outputs and adapts its own configuration to use them. There is
currently no support for merged/split displays.

Note regarding the clock configuration:
The LDB output clock should be absolutely identical to the LCDIF output
clock so both blocks can talk to each other synchronously. However, the
LDB clock has an internal divisor of 7 (respectively 3.5 in dual
configuration) which means the LDB input clock must be explicitly set
once we know the configuration.

This driver was tested on i.MX8MP using a single panel connected to the
LVDS2 interface.

Signed-off-by: Miquel Raynal <miquel.ray...@bootlin.com>
---
 drivers/video/imx/Kconfig  |   4 +
 drivers/video/imx/Makefile |   1 +
 drivers/video/imx/ldb.c    | 251 +++++++++++++++++++++++++++++++++++++
 3 files changed, 256 insertions(+)
 create mode 100644 drivers/video/imx/ldb.c

diff --git a/drivers/video/imx/Kconfig b/drivers/video/imx/Kconfig
index 34e8b640595..f8e0e3b23b3 100644
--- a/drivers/video/imx/Kconfig
+++ b/drivers/video/imx/Kconfig
@@ -13,3 +13,7 @@ config IMX_VIDEO_SKIP
 config IMX_HDMI
        bool "Enable HDMI support in IPUv3"
        depends on VIDEO_IPUV3
+
+config IMX_LDB
+       bool "i.MX LVDS display bridge (LDB)"
+       depends on VIDEO_BRIDGE
diff --git a/drivers/video/imx/Makefile b/drivers/video/imx/Makefile
index 46cc44d64b0..8d106589b75 100644
--- a/drivers/video/imx/Makefile
+++ b/drivers/video/imx/Makefile
@@ -4,3 +4,4 @@
 # Wolfgang Denk, DENX Software Engineering, w...@denx.de.
 
 obj-$(CONFIG_VIDEO_IPUV3) += mxc_ipuv3_fb.o ipu_common.o ipu_disp.o
+obj-$(CONFIG_IMX_LDB) += ldb.o
diff --git a/drivers/video/imx/ldb.c b/drivers/video/imx/ldb.c
new file mode 100644
index 00000000000..10be4421cb5
--- /dev/null
+++ b/drivers/video/imx/ldb.c
@@ -0,0 +1,251 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Derived work from:
+ *   Philippe Cornu <philippe.co...@st.com>
+ *   Yannick Fertre <yannick.fer...@st.com>
+ * Adapted by Miquel Raynal <miquel.ray...@bootlin.com>
+ */
+
+#define LOG_CATEGORY UCLASS_VIDEO_BRIDGE
+
+#include <clk.h>
+#include <dm.h>
+#include <log.h>
+#include <panel.h>
+#include <video_bridge.h>
+#include <asm/io.h>
+#include <linux/delay.h>
+
+#define LDB_CTRL_CH0_ENABLE BIT(0)
+#define LDB_CTRL_CH1_ENABLE BIT(2)
+#define LDB_CTRL_CH0_DATA_WIDTH BIT(5)
+#define LDB_CTRL_CH0_BIT_MAPPING BIT(6)
+#define LDB_CTRL_CH1_DATA_WIDTH BIT(7)
+#define LDB_CTRL_CH1_BIT_MAPPING BIT(8)
+#define LDB_CTRL_DI0_VSYNC_POLARITY BIT(9)
+#define LDB_CTRL_DI1_VSYNC_POLARITY BIT(10)
+
+#define LVDS_CTRL_CH0_EN BIT(0)
+#define LVDS_CTRL_CH1_EN BIT(1)
+#define LVDS_CTRL_VBG_EN BIT(2)
+#define LVDS_CTRL_PRE_EMPH_EN BIT(4)
+#define LVDS_CTRL_PRE_EMPH_ADJ(n) (((n) & 0x7) << 5)
+#define LVDS_CTRL_CC_ADJ(n) (((n) & 0x7) << 11)
+
+struct imx_ldb_priv {
+       struct clk ldb_clk;
+       void __iomem *ldb_ctrl;
+       void __iomem *lvds_ctrl;
+       struct udevice *lvds1;
+       struct udevice *lvds2;
+};
+
+static int imx_ldb_set_backlight(struct udevice *dev, int percent)
+{
+       struct imx_ldb_priv *priv = dev_get_priv(dev);
+       int ret;
+
+       if (priv->lvds1) {
+               ret = panel_enable_backlight(priv->lvds1);
+               if (ret) {
+                       debug("ldb: Cannot enable lvds1 backlight\n");
+                       return ret;
+               }
+
+               ret = panel_set_backlight(priv->lvds1, percent);
+               if (ret)
+                       return ret;
+       }
+
+       if (priv->lvds2) {
+               ret = panel_enable_backlight(priv->lvds2);
+               if (ret) {
+                       debug("ldb: Cannot enable lvds2 backlight\n");
+                       return ret;
+               }
+
+               ret = panel_set_backlight(priv->lvds2, percent);
+               if (ret)
+                       return ret;
+       }
+
+       return 0;
+}
+
+static int imx_ldb_of_to_plat(struct udevice *dev)
+{
+       struct imx_ldb_priv *priv = dev_get_priv(dev);
+       int ret;
+
+       uclass_get_device_by_endpoint(UCLASS_PANEL, dev, 1, &priv->lvds1);
+       uclass_get_device_by_endpoint(UCLASS_PANEL, dev, 2, &priv->lvds2);
+       if (!priv->lvds1 && !priv->lvds2) {
+               debug("ldb: No remote panel for '%s' (ret=%d)\n",
+                     dev_read_name(dev), ret);
+               return ret;
+       }
+
+       return 0;
+}
+
+/* The block has a mysterious x7 internal divisor (x3.5 in dual configuration) 
*/
+#define IMX_LDB_INTERNAL_DIVISOR(x) (((x) * 70) / 10)
+#define IMX_LDB_INTERNAL_DIVISOR_DUAL(x) (((x) * 35) / 10)
+
+static ulong imx_ldb_input_rate(struct imx_ldb_priv *priv,
+                               struct display_timing *timings)
+{
+       ulong target_rate = timings->pixelclock.typ;
+
+       if (priv->lvds1 && priv->lvds2)
+               return IMX_LDB_INTERNAL_DIVISOR_DUAL(target_rate);
+
+       return IMX_LDB_INTERNAL_DIVISOR(target_rate);
+}
+
+static int imx_ldb_attach(struct udevice *dev)
+{
+       struct imx_ldb_priv *priv = dev_get_priv(dev);
+       struct display_timing timings;
+       bool format_jeida = false;
+       bool format_24bpp = true;
+       u32 ldb_ctrl = 0, lvds_ctrl;
+       ulong ldb_rate;
+       int ret;
+
+       /* TODO: update the 24bpp/jeida booleans with proper checks when they
+        * will be supported.
+        */
+       if (priv->lvds1) {
+               ret = panel_get_display_timing(priv->lvds1, &timings);
+               if (ret) {
+                       ret = 
ofnode_decode_display_timing(dev_ofnode(priv->lvds1),
+                                                          0, &timings);
+                       if (ret) {
+                               printf("Cannot decode lvds1 timings (%d)\n", 
ret);
+                               return ret;
+                       }
+               }
+
+               ldb_ctrl |= LDB_CTRL_CH0_ENABLE;
+               if (format_24bpp)
+                       ldb_ctrl |= LDB_CTRL_CH0_DATA_WIDTH;
+               if (format_jeida)
+                       ldb_ctrl |= LDB_CTRL_CH0_BIT_MAPPING;
+               if (timings.flags & DISPLAY_FLAGS_VSYNC_HIGH)
+                       ldb_ctrl |= LDB_CTRL_DI0_VSYNC_POLARITY;
+       }
+
+       if (priv->lvds2) {
+               ret = panel_get_display_timing(priv->lvds2, &timings);
+               if (ret) {
+                       ret = 
ofnode_decode_display_timing(dev_ofnode(priv->lvds2),
+                                                          0, &timings);
+                       if (ret) {
+                               printf("Cannot decode lvds2 timings (%d)\n", 
ret);
+                               return ret;
+                       }
+               }
+
+               ldb_ctrl |= LDB_CTRL_CH1_ENABLE;
+               if (format_24bpp)
+                       ldb_ctrl |= LDB_CTRL_CH1_DATA_WIDTH;
+               if (format_jeida)
+                       ldb_ctrl |= LDB_CTRL_CH1_BIT_MAPPING;
+               if (timings.flags & DISPLAY_FLAGS_VSYNC_HIGH)
+                       ldb_ctrl |= LDB_CTRL_DI1_VSYNC_POLARITY;
+       }
+
+       /*
+        * Not all pixel clocks will work, as the final rate (after internal
+        * integer division) should be identical to the LCDIF clock, otherwise
+        * the rendering will appear resized/shimmering.
+        */
+       ldb_rate = imx_ldb_input_rate(priv, &timings);
+       clk_set_rate(&priv->ldb_clk, ldb_rate);
+
+       writel(ldb_ctrl, priv->ldb_ctrl);
+
+       lvds_ctrl = LVDS_CTRL_CC_ADJ(2) | LVDS_CTRL_PRE_EMPH_EN |
+                   LVDS_CTRL_PRE_EMPH_ADJ(3) | LVDS_CTRL_VBG_EN;
+       writel(lvds_ctrl, priv->lvds_ctrl);
+
+       /* Wait for VBG to stabilize. */
+       udelay(15);
+
+       if (priv->lvds1)
+               lvds_ctrl |= LVDS_CTRL_CH0_EN;
+       if (priv->lvds2)
+               lvds_ctrl |= LVDS_CTRL_CH1_EN;
+
+       writel(lvds_ctrl, priv->lvds_ctrl);
+
+       return 0;
+}
+
+static int imx_ldb_probe(struct udevice *dev)
+{
+       struct imx_ldb_priv *priv = dev_get_priv(dev);
+       struct udevice *parent = dev_get_parent(dev);
+       fdt_addr_t parent_addr, child_addr;
+       int ret;
+
+       ret = clk_get_by_name(dev, "ldb", &priv->ldb_clk);
+       if (ret < 0)
+               return ret;
+
+       parent_addr = dev_read_addr(parent);
+       if (parent_addr == FDT_ADDR_T_NONE)
+               return -EINVAL;
+
+       child_addr = dev_read_addr_name(dev, "ldb");
+       if (child_addr == FDT_ADDR_T_NONE)
+               return -EINVAL;
+
+       priv->ldb_ctrl = map_physmem(parent_addr + child_addr, 0, MAP_NOCACHE);
+       if (!priv->ldb_ctrl)
+               return -EINVAL;
+
+       child_addr = dev_read_addr_name(dev, "lvds");
+       if (child_addr == FDT_ADDR_T_NONE)
+               return -EINVAL;
+
+       priv->lvds_ctrl = map_physmem(parent_addr + child_addr, 0, MAP_NOCACHE);
+       if (!priv->lvds_ctrl)
+               return -EINVAL;
+
+       ret = clk_enable(&priv->ldb_clk);
+       if (ret)
+               return ret;
+
+       ret = video_bridge_set_active(dev, true);
+       if (ret)
+               goto dis_clk;
+
+       return 0;
+
+dis_clk:
+       clk_disable(&priv->ldb_clk);
+
+       return ret;
+}
+
+struct video_bridge_ops imx_ldb_ops = {
+       .attach = imx_ldb_attach,
+       .set_backlight  = imx_ldb_set_backlight,
+};
+
+static const struct udevice_id imx_ldb_ids[] = {
+       { .compatible = "fsl,imx8mp-ldb"},
+       { }
+};
+
+U_BOOT_DRIVER(imx_ldb) = {
+       .name = "imx-lvds-display-bridge",
+       .id = UCLASS_VIDEO_BRIDGE,
+       .of_match = imx_ldb_ids,
+       .probe = imx_ldb_probe,
+       .of_to_plat = imx_ldb_of_to_plat,
+       .ops = &imx_ldb_ops,
+       .priv_auto = sizeof(struct imx_ldb_priv),
+};
-- 
2.43.0

Reply via email to