drm/sun4i: tcon: Add dithering support for RGB565/RGB666 LCD panels
[sfrench/cifs-2.6.git] / drivers / gpu / drm / sun4i / sun4i_tcon.c
index 3fb084f802e298948d2115995dec660d1b1d22b0..4834c90b49127d3e004cf47fd95a00f971c3943a 100644 (file)
 
 #include <drm/drmP.h>
 #include <drm/drm_atomic_helper.h>
+#include <drm/drm_connector.h>
 #include <drm/drm_crtc.h>
 #include <drm/drm_crtc_helper.h>
 #include <drm/drm_encoder.h>
 #include <drm/drm_modes.h>
 #include <drm/drm_of.h>
+#include <drm/drm_panel.h>
 
 #include <uapi/drm/drm_mode.h>
 
@@ -35,6 +37,7 @@
 #include "sun4i_rgb.h"
 #include "sun4i_tcon.h"
 #include "sun6i_mipi_dsi.h"
+#include "sun8i_tcon_top.h"
 #include "sunxi_engine.h"
 
 static struct drm_connector *sun4i_tcon_get_connector(const struct drm_encoder *encoder)
@@ -275,10 +278,64 @@ static void sun4i_tcon0_mode_set_common(struct sun4i_tcon *tcon,
                     SUN4I_TCON0_BASIC0_Y(mode->crtc_vdisplay));
 }
 
+static void sun4i_tcon0_mode_set_dithering(struct sun4i_tcon *tcon,
+                                          const struct drm_connector *connector)
+{
+       u32 bus_format = 0;
+       u32 val = 0;
+
+       /* XXX Would this ever happen? */
+       if (!connector)
+               return;
+
+       /*
+        * FIXME: Undocumented bits
+        *
+        * The whole dithering process and these parameters are not
+        * explained in the vendor documents or BSP kernel code.
+        */
+       regmap_write(tcon->regs, SUN4I_TCON0_FRM_SEED_PR_REG, 0x11111111);
+       regmap_write(tcon->regs, SUN4I_TCON0_FRM_SEED_PG_REG, 0x11111111);
+       regmap_write(tcon->regs, SUN4I_TCON0_FRM_SEED_PB_REG, 0x11111111);
+       regmap_write(tcon->regs, SUN4I_TCON0_FRM_SEED_LR_REG, 0x11111111);
+       regmap_write(tcon->regs, SUN4I_TCON0_FRM_SEED_LG_REG, 0x11111111);
+       regmap_write(tcon->regs, SUN4I_TCON0_FRM_SEED_LB_REG, 0x11111111);
+       regmap_write(tcon->regs, SUN4I_TCON0_FRM_TBL0_REG, 0x01010000);
+       regmap_write(tcon->regs, SUN4I_TCON0_FRM_TBL1_REG, 0x15151111);
+       regmap_write(tcon->regs, SUN4I_TCON0_FRM_TBL2_REG, 0x57575555);
+       regmap_write(tcon->regs, SUN4I_TCON0_FRM_TBL3_REG, 0x7f7f7777);
+
+       /* Do dithering if panel only supports 6 bits per color */
+       if (connector->display_info.bpc == 6)
+               val |= SUN4I_TCON0_FRM_CTL_EN;
+
+       if (connector->display_info.num_bus_formats == 1)
+               bus_format = connector->display_info.bus_formats[0];
+
+       /* Check the connection format */
+       switch (bus_format) {
+       case MEDIA_BUS_FMT_RGB565_1X16:
+               /* R and B components are only 5 bits deep */
+               val |= SUN4I_TCON0_FRM_CTL_MODE_R;
+               val |= SUN4I_TCON0_FRM_CTL_MODE_B;
+       case MEDIA_BUS_FMT_RGB666_1X18:
+       case MEDIA_BUS_FMT_RGB666_1X7X3_SPWG:
+               /* Fall through: enable dithering */
+               val |= SUN4I_TCON0_FRM_CTL_EN;
+               break;
+       }
+
+       /* Write dithering settings */
+       regmap_write(tcon->regs, SUN4I_TCON_FRM_CTL_REG, val);
+}
+
 static void sun4i_tcon0_mode_set_cpu(struct sun4i_tcon *tcon,
-                                    struct mipi_dsi_device *device,
+                                    const struct drm_encoder *encoder,
                                     const struct drm_display_mode *mode)
 {
+       /* TODO support normal CPU interface modes */
+       struct sun6i_dsi *dsi = encoder_to_sun6i_dsi(encoder);
+       struct mipi_dsi_device *device = dsi->device;
        u8 bpp = mipi_dsi_pixel_format_to_bpp(device->format);
        u8 lanes = device->lanes;
        u32 block_space, start_delay;
@@ -289,6 +346,9 @@ static void sun4i_tcon0_mode_set_cpu(struct sun4i_tcon *tcon,
 
        sun4i_tcon0_mode_set_common(tcon, mode);
 
+       /* Set dithering if needed */
+       sun4i_tcon0_mode_set_dithering(tcon, sun4i_tcon_get_connector(encoder));
+
        regmap_update_bits(tcon->regs, SUN4I_TCON0_CTL_REG,
                           SUN4I_TCON0_CTL_IF_MASK,
                           SUN4I_TCON0_CTL_IF_8080);
@@ -354,6 +414,9 @@ static void sun4i_tcon0_mode_set_lvds(struct sun4i_tcon *tcon,
        tcon->dclk_max_div = 7;
        sun4i_tcon0_mode_set_common(tcon, mode);
 
+       /* Set dithering if needed */
+       sun4i_tcon0_mode_set_dithering(tcon, sun4i_tcon_get_connector(encoder));
+
        /* Adjust clock delay */
        clk_delay = sun4i_tcon_get_clk_delay(mode, 0);
        regmap_update_bits(tcon->regs, SUN4I_TCON0_CTL_REG,
@@ -427,6 +490,9 @@ static void sun4i_tcon0_mode_set_rgb(struct sun4i_tcon *tcon,
        tcon->dclk_max_div = 127;
        sun4i_tcon0_mode_set_common(tcon, mode);
 
+       /* Set dithering if needed */
+       sun4i_tcon0_mode_set_dithering(tcon, tcon->panel->connector);
+
        /* Adjust clock delay */
        clk_delay = sun4i_tcon_get_clk_delay(mode, 0);
        regmap_update_bits(tcon->regs, SUN4I_TCON0_CTL_REG,
@@ -474,6 +540,33 @@ static void sun4i_tcon0_mode_set_rgb(struct sun4i_tcon *tcon,
        if (mode->flags & DRM_MODE_FLAG_PVSYNC)
                val |= SUN4I_TCON0_IO_POL_VSYNC_POSITIVE;
 
+       /*
+        * On A20 and similar SoCs, the only way to achieve Positive Edge
+        * (Rising Edge), is setting dclk clock phase to 2/3(240°).
+        * By default TCON works in Negative Edge(Falling Edge),
+        * this is why phase is set to 0 in that case.
+        * Unfortunately there's no way to logically invert dclk through
+        * IO_POL register.
+        * The only acceptable way to work, triple checked with scope,
+        * is using clock phase set to 0° for Negative Edge and set to 240°
+        * for Positive Edge.
+        * On A33 and similar SoCs there would be a 90° phase option,
+        * but it divides also dclk by 2.
+        * Following code is a way to avoid quirks all around TCON
+        * and DOTCLOCK drivers.
+        */
+       if (!IS_ERR(tcon->panel)) {
+               struct drm_panel *panel = tcon->panel;
+               struct drm_connector *connector = panel->connector;
+               struct drm_display_info display_info = connector->display_info;
+
+               if (display_info.bus_flags & DRM_BUS_FLAG_PIXDATA_POSEDGE)
+                       clk_set_phase(tcon->dclk, 240);
+
+               if (display_info.bus_flags & DRM_BUS_FLAG_PIXDATA_NEGEDGE)
+                       clk_set_phase(tcon->dclk, 0);
+       }
+
        regmap_update_bits(tcon->regs, SUN4I_TCON0_IO_POL_REG,
                           SUN4I_TCON0_IO_POL_HSYNC_POSITIVE | SUN4I_TCON0_IO_POL_VSYNC_POSITIVE,
                           val);
@@ -581,16 +674,10 @@ void sun4i_tcon_mode_set(struct sun4i_tcon *tcon,
                         const struct drm_encoder *encoder,
                         const struct drm_display_mode *mode)
 {
-       struct sun6i_dsi *dsi;
-
        switch (encoder->encoder_type) {
        case DRM_MODE_ENCODER_DSI:
-               /*
-                * This is not really elegant, but it's the "cleaner"
-                * way I could think of...
-                */
-               dsi = encoder_to_sun6i_dsi(encoder);
-               sun4i_tcon0_mode_set_cpu(tcon, dsi->device, mode);
+               /* DSI is tied to special case of CPU interface */
+               sun4i_tcon0_mode_set_cpu(tcon, encoder, mode);
                break;
        case DRM_MODE_ENCODER_LVDS:
                sun4i_tcon0_mode_set_lvds(tcon, encoder, mode);
@@ -880,6 +967,36 @@ static struct sunxi_engine *sun4i_tcon_get_engine_by_id(struct sun4i_drv *drv,
        return ERR_PTR(-EINVAL);
 }
 
+static bool sun4i_tcon_connected_to_tcon_top(struct device_node *node)
+{
+       struct device_node *remote;
+       bool ret = false;
+
+       remote = of_graph_get_remote_node(node, 0, -1);
+       if (remote) {
+               ret = !!of_match_node(sun8i_tcon_top_of_table, remote);
+               of_node_put(remote);
+       }
+
+       return ret;
+}
+
+static int sun4i_tcon_get_index(struct sun4i_drv *drv)
+{
+       struct list_head *pos;
+       int size = 0;
+
+       /*
+        * Because TCON is added to the list at the end of the probe
+        * (after this function is called), index of the current TCON
+        * will be same as current TCON list size.
+        */
+       list_for_each(pos, &drv->tcon_list)
+               ++size;
+
+       return size;
+}
+
 /*
  * On SoCs with the old display pipeline design (Display Engine 1.0),
  * we assumed the TCON was always tied to just one backend. However
@@ -928,8 +1045,24 @@ static struct sunxi_engine *sun4i_tcon_find_engine(struct sun4i_drv *drv,
         * connections between the backend and TCON?
         */
        if (of_get_child_count(port) > 1) {
-               /* Get our ID directly from an upstream endpoint */
-               int id = sun4i_tcon_of_get_id_from_port(port);
+               int id;
+
+               /*
+                * When pipeline has the same number of TCONs and engines which
+                * are represented by frontends/backends (DE1) or mixers (DE2),
+                * we match them by their respective IDs. However, if pipeline
+                * contains TCON TOP, chances are that there are either more
+                * TCONs than engines (R40) or TCONs with non-consecutive ids.
+                * (H6). In that case it's easier just use TCON index in list
+                * as an id. That means that on R40, any 2 TCONs can be enabled
+                * in DT out of 4 (there are 2 mixers). Due to the design of
+                * TCON TOP, remaining 2 TCONs can't be connected to anything
+                * anyway.
+                */
+               if (sun4i_tcon_connected_to_tcon_top(node))
+                       id = sun4i_tcon_get_index(drv);
+               else
+                       id = sun4i_tcon_of_get_id_from_port(port);
 
                /* Get our engine by matching our ID */
                engine = sun4i_tcon_get_engine_by_id(drv, id);
@@ -1244,6 +1377,40 @@ static int sun6i_tcon_set_mux(struct sun4i_tcon *tcon,
        return 0;
 }
 
+static int sun8i_r40_tcon_tv_set_mux(struct sun4i_tcon *tcon,
+                                    const struct drm_encoder *encoder)
+{
+       struct device_node *port, *remote;
+       struct platform_device *pdev;
+       int id, ret;
+
+       /* find TCON TOP platform device and TCON id */
+
+       port = of_graph_get_port_by_id(tcon->dev->of_node, 0);
+       if (!port)
+               return -EINVAL;
+
+       id = sun4i_tcon_of_get_id_from_port(port);
+       of_node_put(port);
+
+       remote = of_graph_get_remote_node(tcon->dev->of_node, 0, -1);
+       if (!remote)
+               return -EINVAL;
+
+       pdev = of_find_device_by_node(remote);
+       of_node_put(remote);
+       if (!pdev)
+               return -EINVAL;
+
+       if (encoder->encoder_type == DRM_MODE_ENCODER_TMDS) {
+               ret = sun8i_tcon_top_set_hdmi_src(&pdev->dev, id);
+               if (ret)
+                       return ret;
+       }
+
+       return sun8i_tcon_top_de_config(&pdev->dev, tcon->id, id);
+}
+
 static const struct sun4i_tcon_quirks sun4i_a10_quirks = {
        .has_channel_0          = true,
        .has_channel_1          = true,
@@ -1291,6 +1458,11 @@ static const struct sun4i_tcon_quirks sun8i_a83t_tv_quirks = {
        .has_channel_1          = true,
 };
 
+static const struct sun4i_tcon_quirks sun8i_r40_tv_quirks = {
+       .has_channel_1          = true,
+       .set_mux                = sun8i_r40_tcon_tv_set_mux,
+};
+
 static const struct sun4i_tcon_quirks sun8i_v3s_quirks = {
        .has_channel_0          = true,
 };
@@ -1315,6 +1487,7 @@ const struct of_device_id sun4i_tcon_of_table[] = {
        { .compatible = "allwinner,sun8i-a33-tcon", .data = &sun8i_a33_quirks },
        { .compatible = "allwinner,sun8i-a83t-tcon-lcd", .data = &sun8i_a83t_lcd_quirks },
        { .compatible = "allwinner,sun8i-a83t-tcon-tv", .data = &sun8i_a83t_tv_quirks },
+       { .compatible = "allwinner,sun8i-r40-tcon-tv", .data = &sun8i_r40_tv_quirks },
        { .compatible = "allwinner,sun8i-v3s-tcon", .data = &sun8i_v3s_quirks },
        { .compatible = "allwinner,sun9i-a80-tcon-lcd", .data = &sun9i_a80_tcon_lcd_quirks },
        { .compatible = "allwinner,sun9i-a80-tcon-tv", .data = &sun9i_a80_tcon_tv_quirks },