Both mixer selectors reset to TCON 0. Selecting the same TCON for both
mixers causes black or corrupted output.

When a route would collide, park the other mixer on another described
TCON, or an unused selector if none exists.

Since the TCON index is now used as a shift, also reject negative
values.

Tested on Orange Pi 3 with TCON_LCD0 and TCON_TV0.

Link: https://lore.kernel.org/linux-sunxi/Zn8GVkpwXwhaUFno@titan/
Link: 
https://lore.kernel.org/linux-sunxi/[email protected]/
Fixes: 05db311a792d ("drm/sun4i: tcon-top: Add helpers for mux switching")
Signed-off-by: Jernej Skrabec <[email protected]>
---
 drivers/gpu/drm/sun4i/sun8i_tcon_top.c | 71 +++++++++++++++++++++-----
 drivers/gpu/drm/sun4i/sun8i_tcon_top.h |  6 +++
 2 files changed, 65 insertions(+), 12 deletions(-)

diff --git a/drivers/gpu/drm/sun4i/sun8i_tcon_top.c 
b/drivers/gpu/drm/sun4i/sun8i_tcon_top.c
index 8adda578c51b..9cbd655518b2 100644
--- a/drivers/gpu/drm/sun4i/sun8i_tcon_top.c
+++ b/drivers/gpu/drm/sun4i/sun8i_tcon_top.c
@@ -25,6 +25,49 @@ static bool sun8i_tcon_top_node_is_tcon_top(struct 
device_node *node)
        return !!of_match_node(sun8i_tcon_top_of_table, node);
 }
 
+static unsigned int sun8i_tcon_top_get_tcon_map(struct device_node *node)
+{
+       static const u32 out_ports[] = {
+               TCON_TOP_MIXER0_OUT_PORT,
+               TCON_TOP_MIXER1_OUT_PORT,
+       };
+       unsigned int i, map = 0;
+
+       for (i = 0; i < ARRAY_SIZE(out_ports); i++) {
+               struct device_node *port;
+
+               port = of_graph_get_port_by_id(node, out_ports[i]);
+               if (!port)
+                       continue;
+
+               for_each_of_graph_port_endpoint(port, ep) {
+                       struct of_endpoint endpoint;
+
+                       if (of_graph_parse_endpoint(ep, &endpoint))
+                               continue;
+
+                       if (endpoint.id < TCON_TOP_PORT_TCON_NUM)
+                               map |= BIT(endpoint.id);
+               }
+
+               of_node_put(port);
+       }
+
+       return map;
+}
+
+static unsigned int sun8i_tcon_top_park_index(struct sun8i_tcon_top *tcon_top,
+                                             int tcon)
+{
+       unsigned int candidates;
+
+       candidates = tcon_top->tcon_map & ~BIT(tcon);
+       if (!candidates)
+               candidates = GENMASK(TCON_TOP_PORT_TCON_NUM - 1, 0) & 
~BIT(tcon);
+
+       return ffs(candidates) - 1;
+}
+
 int sun8i_tcon_top_set_hdmi_src(struct device *dev, int tcon)
 {
        struct sun8i_tcon_top *tcon_top = dev_get_drvdata(dev);
@@ -57,6 +100,7 @@ EXPORT_SYMBOL(sun8i_tcon_top_set_hdmi_src);
 int sun8i_tcon_top_de_config(struct device *dev, int mixer, int tcon)
 {
        struct sun8i_tcon_top *tcon_top = dev_get_drvdata(dev);
+       u32 mixer_msk, other_msk;
        unsigned long flags;
        u32 reg;
 
@@ -70,21 +114,27 @@ int sun8i_tcon_top_de_config(struct device *dev, int 
mixer, int tcon)
                return -EINVAL;
        }
 
-       if (tcon > 3) {
-               dev_err(dev, "TCON index is too high!\n");
+       if (tcon < 0 || tcon >= TCON_TOP_PORT_TCON_NUM) {
+               dev_err(dev, "TCON index is invalid!\n");
                return -EINVAL;
        }
 
+       mixer_msk = mixer ? TCON_TOP_PORT_DE1_MSK : TCON_TOP_PORT_DE0_MSK;
+       other_msk = mixer ? TCON_TOP_PORT_DE0_MSK : TCON_TOP_PORT_DE1_MSK;
+
        spin_lock_irqsave(&tcon_top->reg_lock, flags);
 
        reg = readl(tcon_top->regs + TCON_TOP_PORT_SEL_REG);
-       if (mixer == 0) {
-               reg &= ~TCON_TOP_PORT_DE0_MSK;
-               reg |= FIELD_PREP(TCON_TOP_PORT_DE0_MSK, tcon);
-       } else {
-               reg &= ~TCON_TOP_PORT_DE1_MSK;
-               reg |= FIELD_PREP(TCON_TOP_PORT_DE1_MSK, tcon);
+
+       reg &= ~mixer_msk;
+       reg |= field_prep(mixer_msk, tcon);
+
+       if (field_get(other_msk, reg) == tcon) {
+               reg &= ~other_msk;
+               reg |= field_prep(other_msk,
+                                 sun8i_tcon_top_park_index(tcon_top, tcon));
        }
+
        writel(reg, tcon_top->regs + TCON_TOP_PORT_SEL_REG);
 
        spin_unlock_irqrestore(&tcon_top->reg_lock, flags);
@@ -143,6 +193,7 @@ static int sun8i_tcon_top_bind(struct device *dev, struct 
device *master,
                return -ENOMEM;
        clk_data->num = CLK_NUM;
        tcon_top->clk_data = clk_data;
+       tcon_top->tcon_map = sun8i_tcon_top_get_tcon_map(dev->of_node);
 
        spin_lock_init(&tcon_top->reg_lock);
 
@@ -175,10 +226,6 @@ static int sun8i_tcon_top_bind(struct device *dev, struct 
device *master,
                goto err_assert_reset;
        }
 
-       /*
-        * At least on H6, some registers have some bits set by default
-        * which may cause issues. Clear them here.
-        */
        writel(0, regs + TCON_TOP_PORT_SEL_REG);
        writel(0, regs + TCON_TOP_GATE_SRC_REG);
 
diff --git a/drivers/gpu/drm/sun4i/sun8i_tcon_top.h 
b/drivers/gpu/drm/sun4i/sun8i_tcon_top.h
index 0390584a330e..2b887470a49f 100644
--- a/drivers/gpu/drm/sun4i/sun8i_tcon_top.h
+++ b/drivers/gpu/drm/sun4i/sun8i_tcon_top.h
@@ -14,6 +14,10 @@
 #define TCON_TOP_PORT_SEL_REG          0x1C
 #define TCON_TOP_PORT_DE0_MSK                  GENMASK(1, 0)
 #define TCON_TOP_PORT_DE1_MSK                  GENMASK(5, 4)
+#define TCON_TOP_PORT_TCON_NUM                 4
+
+#define TCON_TOP_MIXER0_OUT_PORT               1
+#define TCON_TOP_MIXER1_OUT_PORT               3
 
 #define TCON_TOP_GATE_SRC_REG          0x20
 #define TCON_TOP_HDMI_SRC_MSK                  GENMASK(29, 28)
@@ -29,6 +33,8 @@ struct sun8i_tcon_top {
        void __iomem                    *regs;
        struct reset_control            *rst;
 
+       unsigned int                    tcon_map;
+
        /*
         * spinlock is used to synchronize access to same
         * register where multiple clock gates can be set.
-- 
2.43.0

Reply via email to