forked from Minki/linux
1a396789f6
The Atmel HLCDC (HLCD Controller) IP available on some Atmel SoCs (i.e. at91sam9n12, at91sam9x5 family or sama5d3 family) provides a display controller device. This display controller supports at least one primary plane and might provide several overlays and an hardware cursor depending on the IP version. At the moment, this driver only implements an RGB connector to interface with LCD panels, but support for other kind of external devices might be added later. Signed-off-by: Boris Brezillon <boris.brezillon@free-electrons.com> Reviewed-by: Rob Clark <robdclark@gmail.com> Tested-by: Anthony Harivel <anthony.harivel@emtrion.de> Tested-by: Ludovic Desroches <ludovic.desroches@atmel.com> Acked-by: Nicolas Ferre <nicolas.ferre@atmel.com>
320 lines
8.2 KiB
C
320 lines
8.2 KiB
C
/*
|
|
* Copyright (C) 2014 Traphandler
|
|
* Copyright (C) 2014 Free Electrons
|
|
* Copyright (C) 2014 Atmel
|
|
*
|
|
* Author: Jean-Jacques Hiblot <jjhiblot@traphandler.com>
|
|
* Author: Boris BREZILLON <boris.brezillon@free-electrons.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License version 2 as published by
|
|
* the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
* more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with
|
|
* this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <linux/of_graph.h>
|
|
|
|
#include <drm/drmP.h>
|
|
#include <drm/drm_panel.h>
|
|
|
|
#include "atmel_hlcdc_dc.h"
|
|
|
|
/**
|
|
* Atmel HLCDC RGB output mode
|
|
*/
|
|
enum atmel_hlcdc_connector_rgb_mode {
|
|
ATMEL_HLCDC_CONNECTOR_RGB444,
|
|
ATMEL_HLCDC_CONNECTOR_RGB565,
|
|
ATMEL_HLCDC_CONNECTOR_RGB666,
|
|
ATMEL_HLCDC_CONNECTOR_RGB888,
|
|
};
|
|
|
|
/**
|
|
* Atmel HLCDC RGB connector structure
|
|
*
|
|
* This structure stores RGB slave device information.
|
|
*
|
|
* @connector: DRM connector
|
|
* @encoder: DRM encoder
|
|
* @dc: pointer to the atmel_hlcdc_dc structure
|
|
* @dpms: current DPMS mode
|
|
*/
|
|
struct atmel_hlcdc_rgb_output {
|
|
struct drm_connector connector;
|
|
struct drm_encoder encoder;
|
|
struct atmel_hlcdc_dc *dc;
|
|
int dpms;
|
|
};
|
|
|
|
static inline struct atmel_hlcdc_rgb_output *
|
|
drm_connector_to_atmel_hlcdc_rgb_output(struct drm_connector *connector)
|
|
{
|
|
return container_of(connector, struct atmel_hlcdc_rgb_output,
|
|
connector);
|
|
}
|
|
|
|
static inline struct atmel_hlcdc_rgb_output *
|
|
drm_encoder_to_atmel_hlcdc_rgb_output(struct drm_encoder *encoder)
|
|
{
|
|
return container_of(encoder, struct atmel_hlcdc_rgb_output, encoder);
|
|
}
|
|
|
|
/**
|
|
* Atmel HLCDC Panel device structure
|
|
*
|
|
* This structure is specialization of the slave device structure to
|
|
* interface with drm panels.
|
|
*
|
|
* @base: base slave device fields
|
|
* @panel: drm panel attached to this slave device
|
|
*/
|
|
struct atmel_hlcdc_panel {
|
|
struct atmel_hlcdc_rgb_output base;
|
|
struct drm_panel *panel;
|
|
};
|
|
|
|
static inline struct atmel_hlcdc_panel *
|
|
atmel_hlcdc_rgb_output_to_panel(struct atmel_hlcdc_rgb_output *output)
|
|
{
|
|
return container_of(output, struct atmel_hlcdc_panel, base);
|
|
}
|
|
|
|
static void atmel_hlcdc_panel_encoder_dpms(struct drm_encoder *encoder,
|
|
int mode)
|
|
{
|
|
struct atmel_hlcdc_rgb_output *rgb =
|
|
drm_encoder_to_atmel_hlcdc_rgb_output(encoder);
|
|
struct atmel_hlcdc_panel *panel = atmel_hlcdc_rgb_output_to_panel(rgb);
|
|
|
|
if (mode != DRM_MODE_DPMS_ON)
|
|
mode = DRM_MODE_DPMS_OFF;
|
|
|
|
if (mode == rgb->dpms)
|
|
return;
|
|
|
|
if (mode != DRM_MODE_DPMS_ON)
|
|
drm_panel_disable(panel->panel);
|
|
else
|
|
drm_panel_enable(panel->panel);
|
|
|
|
rgb->dpms = mode;
|
|
}
|
|
|
|
static bool
|
|
atmel_hlcdc_panel_encoder_mode_fixup(struct drm_encoder *encoder,
|
|
const struct drm_display_mode *mode,
|
|
struct drm_display_mode *adjusted)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
static void atmel_hlcdc_panel_encoder_prepare(struct drm_encoder *encoder)
|
|
{
|
|
atmel_hlcdc_panel_encoder_dpms(encoder, DRM_MODE_DPMS_OFF);
|
|
}
|
|
|
|
static void atmel_hlcdc_panel_encoder_commit(struct drm_encoder *encoder)
|
|
{
|
|
atmel_hlcdc_panel_encoder_dpms(encoder, DRM_MODE_DPMS_ON);
|
|
}
|
|
|
|
static void
|
|
atmel_hlcdc_rgb_encoder_mode_set(struct drm_encoder *encoder,
|
|
struct drm_display_mode *mode,
|
|
struct drm_display_mode *adjusted)
|
|
{
|
|
struct atmel_hlcdc_rgb_output *rgb =
|
|
drm_encoder_to_atmel_hlcdc_rgb_output(encoder);
|
|
struct drm_display_info *info = &rgb->connector.display_info;
|
|
unsigned int cfg;
|
|
|
|
cfg = 0;
|
|
|
|
if (info->num_bus_formats) {
|
|
switch (info->bus_formats[0]) {
|
|
case MEDIA_BUS_FMT_RGB666_1X18:
|
|
cfg |= ATMEL_HLCDC_CONNECTOR_RGB666 << 8;
|
|
break;
|
|
case MEDIA_BUS_FMT_RGB888_1X24:
|
|
cfg |= ATMEL_HLCDC_CONNECTOR_RGB888 << 8;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
regmap_update_bits(rgb->dc->hlcdc->regmap, ATMEL_HLCDC_CFG(5),
|
|
ATMEL_HLCDC_MODE_MASK,
|
|
cfg);
|
|
}
|
|
|
|
static struct drm_encoder_helper_funcs atmel_hlcdc_panel_encoder_helper_funcs = {
|
|
.dpms = atmel_hlcdc_panel_encoder_dpms,
|
|
.mode_fixup = atmel_hlcdc_panel_encoder_mode_fixup,
|
|
.prepare = atmel_hlcdc_panel_encoder_prepare,
|
|
.commit = atmel_hlcdc_panel_encoder_commit,
|
|
.mode_set = atmel_hlcdc_rgb_encoder_mode_set,
|
|
};
|
|
|
|
static void atmel_hlcdc_rgb_encoder_destroy(struct drm_encoder *encoder)
|
|
{
|
|
drm_encoder_cleanup(encoder);
|
|
memset(encoder, 0, sizeof(*encoder));
|
|
}
|
|
|
|
static const struct drm_encoder_funcs atmel_hlcdc_panel_encoder_funcs = {
|
|
.destroy = atmel_hlcdc_rgb_encoder_destroy,
|
|
};
|
|
|
|
static int atmel_hlcdc_panel_get_modes(struct drm_connector *connector)
|
|
{
|
|
struct atmel_hlcdc_rgb_output *rgb =
|
|
drm_connector_to_atmel_hlcdc_rgb_output(connector);
|
|
struct atmel_hlcdc_panel *panel = atmel_hlcdc_rgb_output_to_panel(rgb);
|
|
|
|
return panel->panel->funcs->get_modes(panel->panel);
|
|
}
|
|
|
|
static int atmel_hlcdc_rgb_mode_valid(struct drm_connector *connector,
|
|
struct drm_display_mode *mode)
|
|
{
|
|
struct atmel_hlcdc_rgb_output *rgb =
|
|
drm_connector_to_atmel_hlcdc_rgb_output(connector);
|
|
|
|
return atmel_hlcdc_dc_mode_valid(rgb->dc, mode);
|
|
}
|
|
|
|
|
|
|
|
static struct drm_encoder *
|
|
atmel_hlcdc_rgb_best_encoder(struct drm_connector *connector)
|
|
{
|
|
struct atmel_hlcdc_rgb_output *rgb =
|
|
drm_connector_to_atmel_hlcdc_rgb_output(connector);
|
|
|
|
return &rgb->encoder;
|
|
}
|
|
|
|
static struct drm_connector_helper_funcs atmel_hlcdc_panel_connector_helper_funcs = {
|
|
.get_modes = atmel_hlcdc_panel_get_modes,
|
|
.mode_valid = atmel_hlcdc_rgb_mode_valid,
|
|
.best_encoder = atmel_hlcdc_rgb_best_encoder,
|
|
};
|
|
|
|
static enum drm_connector_status
|
|
atmel_hlcdc_panel_connector_detect(struct drm_connector *connector, bool force)
|
|
{
|
|
return connector_status_connected;
|
|
}
|
|
|
|
static void
|
|
atmel_hlcdc_panel_connector_destroy(struct drm_connector *connector)
|
|
{
|
|
struct atmel_hlcdc_rgb_output *rgb =
|
|
drm_connector_to_atmel_hlcdc_rgb_output(connector);
|
|
struct atmel_hlcdc_panel *panel = atmel_hlcdc_rgb_output_to_panel(rgb);
|
|
|
|
drm_panel_detach(panel->panel);
|
|
drm_connector_cleanup(connector);
|
|
}
|
|
|
|
static const struct drm_connector_funcs atmel_hlcdc_panel_connector_funcs = {
|
|
.dpms = drm_helper_connector_dpms,
|
|
.detect = atmel_hlcdc_panel_connector_detect,
|
|
.fill_modes = drm_helper_probe_single_connector_modes,
|
|
.destroy = atmel_hlcdc_panel_connector_destroy,
|
|
};
|
|
|
|
static int atmel_hlcdc_create_panel_output(struct drm_device *dev,
|
|
struct of_endpoint *ep)
|
|
{
|
|
struct atmel_hlcdc_dc *dc = dev->dev_private;
|
|
struct device_node *np;
|
|
struct drm_panel *p = NULL;
|
|
struct atmel_hlcdc_panel *panel;
|
|
int ret;
|
|
|
|
np = of_graph_get_remote_port_parent(ep->local_node);
|
|
if (!np)
|
|
return -EINVAL;
|
|
|
|
p = of_drm_find_panel(np);
|
|
of_node_put(np);
|
|
|
|
if (!p)
|
|
return -EPROBE_DEFER;
|
|
|
|
panel = devm_kzalloc(dev->dev, sizeof(*panel), GFP_KERNEL);
|
|
if (!panel)
|
|
return -EINVAL;
|
|
|
|
panel->base.dpms = DRM_MODE_DPMS_OFF;
|
|
|
|
panel->base.dc = dc;
|
|
|
|
drm_encoder_helper_add(&panel->base.encoder,
|
|
&atmel_hlcdc_panel_encoder_helper_funcs);
|
|
ret = drm_encoder_init(dev, &panel->base.encoder,
|
|
&atmel_hlcdc_panel_encoder_funcs,
|
|
DRM_MODE_ENCODER_LVDS);
|
|
if (ret)
|
|
return ret;
|
|
|
|
panel->base.connector.dpms = DRM_MODE_DPMS_OFF;
|
|
panel->base.connector.polled = DRM_CONNECTOR_POLL_CONNECT;
|
|
drm_connector_helper_add(&panel->base.connector,
|
|
&atmel_hlcdc_panel_connector_helper_funcs);
|
|
ret = drm_connector_init(dev, &panel->base.connector,
|
|
&atmel_hlcdc_panel_connector_funcs,
|
|
DRM_MODE_CONNECTOR_LVDS);
|
|
if (ret)
|
|
goto err_encoder_cleanup;
|
|
|
|
drm_mode_connector_attach_encoder(&panel->base.connector,
|
|
&panel->base.encoder);
|
|
panel->base.encoder.possible_crtcs = 0x1;
|
|
|
|
drm_panel_attach(p, &panel->base.connector);
|
|
panel->panel = p;
|
|
|
|
return 0;
|
|
|
|
err_encoder_cleanup:
|
|
drm_encoder_cleanup(&panel->base.encoder);
|
|
|
|
return ret;
|
|
}
|
|
|
|
int atmel_hlcdc_create_outputs(struct drm_device *dev)
|
|
{
|
|
struct device_node *port_np, *np;
|
|
struct of_endpoint ep;
|
|
int ret;
|
|
|
|
port_np = of_get_child_by_name(dev->dev->of_node, "port");
|
|
if (!port_np)
|
|
return -EINVAL;
|
|
|
|
np = of_get_child_by_name(port_np, "endpoint");
|
|
of_node_put(port_np);
|
|
|
|
if (!np)
|
|
return -EINVAL;
|
|
|
|
ret = of_graph_parse_endpoint(np, &ep);
|
|
of_node_put(port_np);
|
|
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* We currently only support panel output */
|
|
return atmel_hlcdc_create_panel_output(dev, &ep);
|
|
}
|