drm: rcar-du: Convert LVDS encoder code to bridge driver
The LVDS encoders used to be described in DT as part of the DU. They now have their own DT node, linked to the DU using the OF graph bindings. This allows moving internal LVDS encoder support to a separate driver modelled as a DRM bridge. Backward compatibility is retained as legacy DT is patched live to move to the new bindings. Signed-off-by: Laurent Pinchart <laurent.pinchart+renesas@ideasonboard.com> Reviewed-by: Niklas Söderlund <niklas.soderlund+renesas@ragnatech.se>
This commit is contained in:
parent
81c0e3dd82
commit
c6a27fa41f
|
@ -19,8 +19,8 @@ config DRM_RCAR_DW_HDMI
|
|||
Enable support for R-Car Gen3 internal HDMI encoder.
|
||||
|
||||
config DRM_RCAR_LVDS
|
||||
bool "R-Car DU LVDS Encoder Support"
|
||||
depends on DRM_RCAR_DU
|
||||
tristate "R-Car DU LVDS Encoder Support"
|
||||
depends on DRM && DRM_BRIDGE && OF
|
||||
select DRM_PANEL
|
||||
select OF_FLATTREE
|
||||
select OF_OVERLAY
|
||||
|
|
|
@ -4,10 +4,8 @@ rcar-du-drm-y := rcar_du_crtc.o \
|
|||
rcar_du_encoder.o \
|
||||
rcar_du_group.o \
|
||||
rcar_du_kms.o \
|
||||
rcar_du_lvdscon.o \
|
||||
rcar_du_plane.o
|
||||
|
||||
rcar-du-drm-$(CONFIG_DRM_RCAR_LVDS) += rcar_du_lvdsenc.o
|
||||
rcar-du-drm-$(CONFIG_DRM_RCAR_LVDS) += rcar_du_of.o \
|
||||
rcar_du_of_lvds_r8a7790.dtb.o \
|
||||
rcar_du_of_lvds_r8a7791.dtb.o \
|
||||
|
@ -18,3 +16,4 @@ rcar-du-drm-$(CONFIG_DRM_RCAR_VSP) += rcar_du_vsp.o
|
|||
|
||||
obj-$(CONFIG_DRM_RCAR_DU) += rcar-du-drm.o
|
||||
obj-$(CONFIG_DRM_RCAR_DW_HDMI) += rcar_dw_hdmi.o
|
||||
obj-$(CONFIG_DRM_RCAR_LVDS) += rcar_lvds.o
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
|
||||
#include "rcar_du_drv.h"
|
||||
#include "rcar_du_kms.h"
|
||||
#include "rcar_du_of.h"
|
||||
#include "rcar_du_regs.h"
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
|
@ -74,7 +75,6 @@ static const struct rcar_du_device_info rzg1_du_r8a7745_info = {
|
|||
.port = 1,
|
||||
},
|
||||
},
|
||||
.num_lvds = 0,
|
||||
};
|
||||
|
||||
static const struct rcar_du_device_info rcar_du_r8a7779_info = {
|
||||
|
@ -95,14 +95,13 @@ static const struct rcar_du_device_info rcar_du_r8a7779_info = {
|
|||
.port = 1,
|
||||
},
|
||||
},
|
||||
.num_lvds = 0,
|
||||
};
|
||||
|
||||
static const struct rcar_du_device_info rcar_du_r8a7790_info = {
|
||||
.gen = 2,
|
||||
.features = RCAR_DU_FEATURE_CRTC_IRQ_CLOCK
|
||||
| RCAR_DU_FEATURE_EXT_CTRL_REGS,
|
||||
.quirks = RCAR_DU_QUIRK_ALIGN_128B | RCAR_DU_QUIRK_LVDS_LANES,
|
||||
.quirks = RCAR_DU_QUIRK_ALIGN_128B,
|
||||
.num_crtcs = 3,
|
||||
.routes = {
|
||||
/*
|
||||
|
@ -164,7 +163,6 @@ static const struct rcar_du_device_info rcar_du_r8a7792_info = {
|
|||
.port = 1,
|
||||
},
|
||||
},
|
||||
.num_lvds = 0,
|
||||
};
|
||||
|
||||
static const struct rcar_du_device_info rcar_du_r8a7794_info = {
|
||||
|
@ -186,7 +184,6 @@ static const struct rcar_du_device_info rcar_du_r8a7794_info = {
|
|||
.port = 1,
|
||||
},
|
||||
},
|
||||
.num_lvds = 0,
|
||||
};
|
||||
|
||||
static const struct rcar_du_device_info rcar_du_r8a7795_info = {
|
||||
|
@ -434,7 +431,19 @@ static struct platform_driver rcar_du_platform_driver = {
|
|||
},
|
||||
};
|
||||
|
||||
module_platform_driver(rcar_du_platform_driver);
|
||||
static int __init rcar_du_init(void)
|
||||
{
|
||||
rcar_du_of_init(rcar_du_of_table);
|
||||
|
||||
return platform_driver_register(&rcar_du_platform_driver);
|
||||
}
|
||||
module_init(rcar_du_init);
|
||||
|
||||
static void __exit rcar_du_exit(void)
|
||||
{
|
||||
platform_driver_unregister(&rcar_du_platform_driver);
|
||||
}
|
||||
module_exit(rcar_du_exit);
|
||||
|
||||
MODULE_AUTHOR("Laurent Pinchart <laurent.pinchart@ideasonboard.com>");
|
||||
MODULE_DESCRIPTION("Renesas R-Car Display Unit DRM Driver");
|
||||
|
|
|
@ -26,14 +26,12 @@ struct device;
|
|||
struct drm_device;
|
||||
struct drm_fbdev_cma;
|
||||
struct rcar_du_device;
|
||||
struct rcar_du_lvdsenc;
|
||||
|
||||
#define RCAR_DU_FEATURE_CRTC_IRQ_CLOCK (1 << 0) /* Per-CRTC IRQ and clock */
|
||||
#define RCAR_DU_FEATURE_EXT_CTRL_REGS (1 << 1) /* Has extended control registers */
|
||||
#define RCAR_DU_FEATURE_VSP1_SOURCE (1 << 2) /* Has inputs from VSP1 */
|
||||
|
||||
#define RCAR_DU_QUIRK_ALIGN_128B (1 << 0) /* Align pitches to 128 bytes */
|
||||
#define RCAR_DU_QUIRK_LVDS_LANES (1 << 1) /* LVDS lanes 1 and 3 inverted */
|
||||
|
||||
/*
|
||||
* struct rcar_du_output_routing - Output routing specification
|
||||
|
@ -70,7 +68,6 @@ struct rcar_du_device_info {
|
|||
|
||||
#define RCAR_DU_MAX_CRTCS 4
|
||||
#define RCAR_DU_MAX_GROUPS DIV_ROUND_UP(RCAR_DU_MAX_CRTCS, 2)
|
||||
#define RCAR_DU_MAX_LVDS 2
|
||||
#define RCAR_DU_MAX_VSPS 4
|
||||
|
||||
struct rcar_du_device {
|
||||
|
@ -96,8 +93,6 @@ struct rcar_du_device {
|
|||
|
||||
unsigned int dpad0_source;
|
||||
unsigned int vspd1_sink;
|
||||
|
||||
struct rcar_du_lvdsenc *lvds[RCAR_DU_MAX_LVDS];
|
||||
};
|
||||
|
||||
static inline bool rcar_du_has(struct rcar_du_device *rcdu,
|
||||
|
|
|
@ -21,134 +21,22 @@
|
|||
#include "rcar_du_drv.h"
|
||||
#include "rcar_du_encoder.h"
|
||||
#include "rcar_du_kms.h"
|
||||
#include "rcar_du_lvdscon.h"
|
||||
#include "rcar_du_lvdsenc.h"
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* Encoder
|
||||
*/
|
||||
|
||||
static void rcar_du_encoder_disable(struct drm_encoder *encoder)
|
||||
{
|
||||
struct rcar_du_encoder *renc = to_rcar_encoder(encoder);
|
||||
|
||||
if (renc->connector && renc->connector->panel) {
|
||||
drm_panel_disable(renc->connector->panel);
|
||||
drm_panel_unprepare(renc->connector->panel);
|
||||
}
|
||||
|
||||
if (renc->lvds)
|
||||
rcar_du_lvdsenc_enable(renc->lvds, encoder->crtc, false);
|
||||
}
|
||||
|
||||
static void rcar_du_encoder_enable(struct drm_encoder *encoder)
|
||||
{
|
||||
struct rcar_du_encoder *renc = to_rcar_encoder(encoder);
|
||||
|
||||
if (renc->lvds)
|
||||
rcar_du_lvdsenc_enable(renc->lvds, encoder->crtc, true);
|
||||
|
||||
if (renc->connector && renc->connector->panel) {
|
||||
drm_panel_prepare(renc->connector->panel);
|
||||
drm_panel_enable(renc->connector->panel);
|
||||
}
|
||||
}
|
||||
|
||||
static int rcar_du_encoder_atomic_check(struct drm_encoder *encoder,
|
||||
struct drm_crtc_state *crtc_state,
|
||||
struct drm_connector_state *conn_state)
|
||||
{
|
||||
struct rcar_du_encoder *renc = to_rcar_encoder(encoder);
|
||||
struct drm_display_mode *adjusted_mode = &crtc_state->adjusted_mode;
|
||||
const struct drm_display_mode *mode = &crtc_state->mode;
|
||||
struct drm_connector *connector = conn_state->connector;
|
||||
struct drm_device *dev = encoder->dev;
|
||||
|
||||
/*
|
||||
* Only panel-related encoder types require validation here, everything
|
||||
* else is handled by the bridge drivers.
|
||||
*/
|
||||
if (connector->connector_type == DRM_MODE_CONNECTOR_LVDS) {
|
||||
const struct drm_display_mode *panel_mode;
|
||||
|
||||
if (list_empty(&connector->modes)) {
|
||||
dev_dbg(dev->dev, "encoder: empty modes list\n");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
panel_mode = list_first_entry(&connector->modes,
|
||||
struct drm_display_mode, head);
|
||||
|
||||
/* We're not allowed to modify the resolution. */
|
||||
if (mode->hdisplay != panel_mode->hdisplay ||
|
||||
mode->vdisplay != panel_mode->vdisplay)
|
||||
return -EINVAL;
|
||||
|
||||
/*
|
||||
* The flat panel mode is fixed, just copy it to the adjusted
|
||||
* mode.
|
||||
*/
|
||||
drm_mode_copy(adjusted_mode, panel_mode);
|
||||
}
|
||||
|
||||
if (renc->lvds)
|
||||
rcar_du_lvdsenc_atomic_check(renc->lvds, adjusted_mode);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void rcar_du_encoder_mode_set(struct drm_encoder *encoder,
|
||||
struct drm_crtc_state *crtc_state,
|
||||
struct drm_connector_state *conn_state)
|
||||
{
|
||||
struct rcar_du_encoder *renc = to_rcar_encoder(encoder);
|
||||
struct drm_display_info *info = &conn_state->connector->display_info;
|
||||
enum rcar_lvds_mode mode;
|
||||
|
||||
rcar_du_crtc_route_output(crtc_state->crtc, renc->output);
|
||||
|
||||
if (!renc->lvds) {
|
||||
/*
|
||||
* The DU driver creates connectors only for the outputs of the
|
||||
* internal LVDS encoders.
|
||||
*/
|
||||
renc->connector = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
renc->connector = to_rcar_connector(conn_state->connector);
|
||||
|
||||
if (!info->num_bus_formats || !info->bus_formats) {
|
||||
dev_err(encoder->dev->dev, "no LVDS bus format reported\n");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (info->bus_formats[0]) {
|
||||
case MEDIA_BUS_FMT_RGB666_1X7X3_SPWG:
|
||||
case MEDIA_BUS_FMT_RGB888_1X7X4_JEIDA:
|
||||
mode = RCAR_LVDS_MODE_JEIDA;
|
||||
break;
|
||||
case MEDIA_BUS_FMT_RGB888_1X7X4_SPWG:
|
||||
mode = RCAR_LVDS_MODE_VESA;
|
||||
break;
|
||||
default:
|
||||
dev_err(encoder->dev->dev,
|
||||
"unsupported LVDS bus format 0x%04x\n",
|
||||
info->bus_formats[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (info->bus_flags & DRM_BUS_FLAG_DATA_LSB_TO_MSB)
|
||||
mode |= RCAR_LVDS_MODE_MIRROR;
|
||||
|
||||
rcar_du_lvdsenc_set_mode(renc->lvds, mode);
|
||||
}
|
||||
|
||||
static const struct drm_encoder_helper_funcs encoder_helper_funcs = {
|
||||
.atomic_mode_set = rcar_du_encoder_mode_set,
|
||||
.disable = rcar_du_encoder_disable,
|
||||
.enable = rcar_du_encoder_enable,
|
||||
.atomic_check = rcar_du_encoder_atomic_check,
|
||||
};
|
||||
|
||||
static const struct drm_encoder_funcs encoder_funcs = {
|
||||
|
@ -172,33 +60,14 @@ int rcar_du_encoder_init(struct rcar_du_device *rcdu,
|
|||
renc->output = output;
|
||||
encoder = rcar_encoder_to_drm_encoder(renc);
|
||||
|
||||
switch (output) {
|
||||
case RCAR_DU_OUTPUT_LVDS0:
|
||||
renc->lvds = rcdu->lvds[0];
|
||||
break;
|
||||
dev_dbg(rcdu->dev, "initializing encoder %pOF for output %u\n",
|
||||
enc_node, output);
|
||||
|
||||
case RCAR_DU_OUTPUT_LVDS1:
|
||||
renc->lvds = rcdu->lvds[1];
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (enc_node) {
|
||||
dev_dbg(rcdu->dev, "initializing encoder %pOF for output %u\n",
|
||||
enc_node, output);
|
||||
|
||||
/* Locate the DRM bridge from the encoder DT node. */
|
||||
bridge = of_drm_find_bridge(enc_node);
|
||||
if (!bridge) {
|
||||
ret = -EPROBE_DEFER;
|
||||
goto done;
|
||||
}
|
||||
} else {
|
||||
dev_dbg(rcdu->dev,
|
||||
"initializing internal encoder for output %u\n",
|
||||
output);
|
||||
/* Locate the DRM bridge from the encoder DT node. */
|
||||
bridge = of_drm_find_bridge(enc_node);
|
||||
if (!bridge) {
|
||||
ret = -EPROBE_DEFER;
|
||||
goto done;
|
||||
}
|
||||
|
||||
ret = drm_encoder_init(rcdu->ddev, encoder, &encoder_funcs,
|
||||
|
@ -208,28 +77,14 @@ int rcar_du_encoder_init(struct rcar_du_device *rcdu,
|
|||
|
||||
drm_encoder_helper_add(encoder, &encoder_helper_funcs);
|
||||
|
||||
if (bridge) {
|
||||
/*
|
||||
* Attach the bridge to the encoder. The bridge will create the
|
||||
* connector.
|
||||
*/
|
||||
ret = drm_bridge_attach(encoder, bridge, NULL);
|
||||
if (ret) {
|
||||
drm_encoder_cleanup(encoder);
|
||||
return ret;
|
||||
}
|
||||
} else {
|
||||
/* There's no bridge, create the connector manually. */
|
||||
switch (output) {
|
||||
case RCAR_DU_OUTPUT_LVDS0:
|
||||
case RCAR_DU_OUTPUT_LVDS1:
|
||||
ret = rcar_du_lvds_connector_init(rcdu, renc, con_node);
|
||||
break;
|
||||
|
||||
default:
|
||||
ret = -EINVAL;
|
||||
break;
|
||||
}
|
||||
/*
|
||||
* Attach the bridge to the encoder. The bridge will create the
|
||||
* connector.
|
||||
*/
|
||||
ret = drm_bridge_attach(encoder, bridge, NULL);
|
||||
if (ret) {
|
||||
drm_encoder_cleanup(encoder);
|
||||
return ret;
|
||||
}
|
||||
|
||||
done:
|
||||
|
|
|
@ -19,13 +19,10 @@
|
|||
|
||||
struct drm_panel;
|
||||
struct rcar_du_device;
|
||||
struct rcar_du_lvdsenc;
|
||||
|
||||
struct rcar_du_encoder {
|
||||
struct drm_encoder base;
|
||||
enum rcar_du_output output;
|
||||
struct rcar_du_connector *connector;
|
||||
struct rcar_du_lvdsenc *lvds;
|
||||
};
|
||||
|
||||
#define to_rcar_encoder(e) \
|
||||
|
@ -33,15 +30,6 @@ struct rcar_du_encoder {
|
|||
|
||||
#define rcar_encoder_to_drm_encoder(e) (&(e)->base)
|
||||
|
||||
struct rcar_du_connector {
|
||||
struct drm_connector connector;
|
||||
struct rcar_du_encoder *encoder;
|
||||
struct drm_panel *panel;
|
||||
};
|
||||
|
||||
#define to_rcar_connector(c) \
|
||||
container_of(c, struct rcar_du_connector, connector)
|
||||
|
||||
int rcar_du_encoder_init(struct rcar_du_device *rcdu,
|
||||
enum rcar_du_output output,
|
||||
struct device_node *enc_node,
|
||||
|
|
|
@ -27,7 +27,6 @@
|
|||
#include "rcar_du_drv.h"
|
||||
#include "rcar_du_encoder.h"
|
||||
#include "rcar_du_kms.h"
|
||||
#include "rcar_du_lvdsenc.h"
|
||||
#include "rcar_du_regs.h"
|
||||
#include "rcar_du_vsp.h"
|
||||
|
||||
|
@ -341,11 +340,10 @@ static int rcar_du_encoders_init_one(struct rcar_du_device *rcdu,
|
|||
of_node_put(entity_ep_node);
|
||||
|
||||
if (!encoder) {
|
||||
/*
|
||||
* If no encoder has been found the entity must be the
|
||||
* connector.
|
||||
*/
|
||||
connector = entity;
|
||||
dev_warn(rcdu->dev,
|
||||
"no encoder found for endpoint %pOF, skipping\n",
|
||||
ep->local_node);
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
ret = rcar_du_encoder_init(rcdu, output, encoder, connector);
|
||||
|
@ -595,10 +593,6 @@ int rcar_du_modeset_init(struct rcar_du_device *rcdu)
|
|||
}
|
||||
|
||||
/* Initialize the encoders. */
|
||||
ret = rcar_du_lvdsenc_init(rcdu);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
|
||||
ret = rcar_du_encoders_init(rcdu);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
|
|
|
@ -1,93 +0,0 @@
|
|||
/*
|
||||
* rcar_du_lvdscon.c -- R-Car Display Unit LVDS Connector
|
||||
*
|
||||
* Copyright (C) 2013-2014 Renesas Electronics Corporation
|
||||
*
|
||||
* Contact: Laurent Pinchart (laurent.pinchart@ideasonboard.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
#include <drm/drmP.h>
|
||||
#include <drm/drm_atomic_helper.h>
|
||||
#include <drm/drm_crtc.h>
|
||||
#include <drm/drm_crtc_helper.h>
|
||||
#include <drm/drm_panel.h>
|
||||
|
||||
#include <video/display_timing.h>
|
||||
#include <video/of_display_timing.h>
|
||||
#include <video/videomode.h>
|
||||
|
||||
#include "rcar_du_drv.h"
|
||||
#include "rcar_du_encoder.h"
|
||||
#include "rcar_du_kms.h"
|
||||
#include "rcar_du_lvdscon.h"
|
||||
|
||||
static int rcar_du_lvds_connector_get_modes(struct drm_connector *connector)
|
||||
{
|
||||
struct rcar_du_connector *rcon = to_rcar_connector(connector);
|
||||
|
||||
return drm_panel_get_modes(rcon->panel);
|
||||
}
|
||||
|
||||
static const struct drm_connector_helper_funcs connector_helper_funcs = {
|
||||
.get_modes = rcar_du_lvds_connector_get_modes,
|
||||
};
|
||||
|
||||
static void rcar_du_lvds_connector_destroy(struct drm_connector *connector)
|
||||
{
|
||||
struct rcar_du_connector *rcon = to_rcar_connector(connector);
|
||||
|
||||
drm_panel_detach(rcon->panel);
|
||||
drm_connector_cleanup(connector);
|
||||
}
|
||||
|
||||
static const struct drm_connector_funcs connector_funcs = {
|
||||
.reset = drm_atomic_helper_connector_reset,
|
||||
.fill_modes = drm_helper_probe_single_connector_modes,
|
||||
.destroy = rcar_du_lvds_connector_destroy,
|
||||
.atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state,
|
||||
.atomic_destroy_state = drm_atomic_helper_connector_destroy_state,
|
||||
};
|
||||
|
||||
int rcar_du_lvds_connector_init(struct rcar_du_device *rcdu,
|
||||
struct rcar_du_encoder *renc,
|
||||
const struct device_node *np)
|
||||
{
|
||||
struct drm_encoder *encoder = rcar_encoder_to_drm_encoder(renc);
|
||||
struct rcar_du_connector *rcon;
|
||||
struct drm_connector *connector;
|
||||
int ret;
|
||||
|
||||
rcon = devm_kzalloc(rcdu->dev, sizeof(*rcon), GFP_KERNEL);
|
||||
if (rcon == NULL)
|
||||
return -ENOMEM;
|
||||
|
||||
connector = &rcon->connector;
|
||||
|
||||
rcon->panel = of_drm_find_panel(np);
|
||||
if (!rcon->panel)
|
||||
return -EPROBE_DEFER;
|
||||
|
||||
ret = drm_connector_init(rcdu->ddev, connector, &connector_funcs,
|
||||
DRM_MODE_CONNECTOR_LVDS);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
|
||||
drm_connector_helper_add(connector, &connector_helper_funcs);
|
||||
|
||||
ret = drm_mode_connector_attach_encoder(connector, encoder);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
|
||||
ret = drm_panel_attach(rcon->panel, connector);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
|
||||
rcon->encoder = renc;
|
||||
|
||||
return 0;
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
/*
|
||||
* rcar_du_lvdscon.h -- R-Car Display Unit LVDS Connector
|
||||
*
|
||||
* Copyright (C) 2013-2014 Renesas Electronics Corporation
|
||||
*
|
||||
* Contact: Laurent Pinchart (laurent.pinchart@ideasonboard.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
#ifndef __RCAR_DU_LVDSCON_H__
|
||||
#define __RCAR_DU_LVDSCON_H__
|
||||
|
||||
struct rcar_du_device;
|
||||
struct rcar_du_encoder;
|
||||
|
||||
int rcar_du_lvds_connector_init(struct rcar_du_device *rcdu,
|
||||
struct rcar_du_encoder *renc,
|
||||
const struct device_node *np);
|
||||
|
||||
#endif /* __RCAR_DU_LVDSCON_H__ */
|
|
@ -1,238 +0,0 @@
|
|||
/*
|
||||
* rcar_du_lvdsenc.c -- R-Car Display Unit LVDS Encoder
|
||||
*
|
||||
* Copyright (C) 2013-2014 Renesas Electronics Corporation
|
||||
*
|
||||
* Contact: Laurent Pinchart (laurent.pinchart@ideasonboard.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
#include <linux/clk.h>
|
||||
#include <linux/delay.h>
|
||||
#include <linux/io.h>
|
||||
#include <linux/platform_device.h>
|
||||
#include <linux/slab.h>
|
||||
|
||||
#include "rcar_du_drv.h"
|
||||
#include "rcar_du_encoder.h"
|
||||
#include "rcar_du_lvdsenc.h"
|
||||
#include "rcar_lvds_regs.h"
|
||||
|
||||
struct rcar_du_lvdsenc {
|
||||
struct rcar_du_device *dev;
|
||||
|
||||
unsigned int index;
|
||||
void __iomem *mmio;
|
||||
struct clk *clock;
|
||||
bool enabled;
|
||||
|
||||
enum rcar_lvds_input input;
|
||||
enum rcar_lvds_mode mode;
|
||||
};
|
||||
|
||||
static void rcar_lvds_write(struct rcar_du_lvdsenc *lvds, u32 reg, u32 data)
|
||||
{
|
||||
iowrite32(data, lvds->mmio + reg);
|
||||
}
|
||||
|
||||
static u32 rcar_lvds_lvdpllcr_gen2(unsigned int freq)
|
||||
{
|
||||
if (freq < 39000)
|
||||
return LVDPLLCR_CEEN | LVDPLLCR_COSEL | LVDPLLCR_PLLDLYCNT_38M;
|
||||
else if (freq < 61000)
|
||||
return LVDPLLCR_CEEN | LVDPLLCR_COSEL | LVDPLLCR_PLLDLYCNT_60M;
|
||||
else if (freq < 121000)
|
||||
return LVDPLLCR_CEEN | LVDPLLCR_COSEL | LVDPLLCR_PLLDLYCNT_121M;
|
||||
else
|
||||
return LVDPLLCR_PLLDLYCNT_150M;
|
||||
}
|
||||
|
||||
static u32 rcar_lvds_lvdpllcr_gen3(unsigned int freq)
|
||||
{
|
||||
if (freq < 42000)
|
||||
return LVDPLLCR_PLLDIVCNT_42M;
|
||||
else if (freq < 85000)
|
||||
return LVDPLLCR_PLLDIVCNT_85M;
|
||||
else if (freq < 128000)
|
||||
return LVDPLLCR_PLLDIVCNT_128M;
|
||||
else
|
||||
return LVDPLLCR_PLLDIVCNT_148M;
|
||||
}
|
||||
|
||||
static int rcar_du_lvdsenc_start(struct rcar_du_lvdsenc *lvds,
|
||||
struct rcar_du_crtc *rcrtc)
|
||||
{
|
||||
const struct drm_display_mode *mode = &rcrtc->crtc.mode;
|
||||
u32 lvdpllcr;
|
||||
u32 lvdhcr;
|
||||
u32 lvdcr0;
|
||||
int ret;
|
||||
|
||||
if (lvds->enabled)
|
||||
return 0;
|
||||
|
||||
ret = clk_prepare_enable(lvds->clock);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
|
||||
/*
|
||||
* Hardcode the channels and control signals routing for now.
|
||||
*
|
||||
* HSYNC -> CTRL0
|
||||
* VSYNC -> CTRL1
|
||||
* DISP -> CTRL2
|
||||
* 0 -> CTRL3
|
||||
*/
|
||||
rcar_lvds_write(lvds, LVDCTRCR, LVDCTRCR_CTR3SEL_ZERO |
|
||||
LVDCTRCR_CTR2SEL_DISP | LVDCTRCR_CTR1SEL_VSYNC |
|
||||
LVDCTRCR_CTR0SEL_HSYNC);
|
||||
|
||||
if (rcar_du_needs(lvds->dev, RCAR_DU_QUIRK_LVDS_LANES))
|
||||
lvdhcr = LVDCHCR_CHSEL_CH(0, 0) | LVDCHCR_CHSEL_CH(1, 3)
|
||||
| LVDCHCR_CHSEL_CH(2, 2) | LVDCHCR_CHSEL_CH(3, 1);
|
||||
else
|
||||
lvdhcr = LVDCHCR_CHSEL_CH(0, 0) | LVDCHCR_CHSEL_CH(1, 1)
|
||||
| LVDCHCR_CHSEL_CH(2, 2) | LVDCHCR_CHSEL_CH(3, 3);
|
||||
|
||||
rcar_lvds_write(lvds, LVDCHCR, lvdhcr);
|
||||
|
||||
/* PLL clock configuration. */
|
||||
if (lvds->dev->info->gen < 3)
|
||||
lvdpllcr = rcar_lvds_lvdpllcr_gen2(mode->clock);
|
||||
else
|
||||
lvdpllcr = rcar_lvds_lvdpllcr_gen3(mode->clock);
|
||||
rcar_lvds_write(lvds, LVDPLLCR, lvdpllcr);
|
||||
|
||||
/* Set the LVDS mode and select the input. */
|
||||
lvdcr0 = lvds->mode << LVDCR0_LVMD_SHIFT;
|
||||
if (rcrtc->index == 2)
|
||||
lvdcr0 |= LVDCR0_DUSEL;
|
||||
rcar_lvds_write(lvds, LVDCR0, lvdcr0);
|
||||
|
||||
/* Turn all the channels on. */
|
||||
rcar_lvds_write(lvds, LVDCR1,
|
||||
LVDCR1_CHSTBY(3) | LVDCR1_CHSTBY(2) |
|
||||
LVDCR1_CHSTBY(1) | LVDCR1_CHSTBY(0) | LVDCR1_CLKSTBY);
|
||||
|
||||
if (lvds->dev->info->gen < 3) {
|
||||
/* Enable LVDS operation and turn the bias circuitry on. */
|
||||
lvdcr0 |= LVDCR0_BEN | LVDCR0_LVEN;
|
||||
rcar_lvds_write(lvds, LVDCR0, lvdcr0);
|
||||
}
|
||||
|
||||
/* Turn the PLL on. */
|
||||
lvdcr0 |= LVDCR0_PLLON;
|
||||
rcar_lvds_write(lvds, LVDCR0, lvdcr0);
|
||||
|
||||
if (lvds->dev->info->gen > 2) {
|
||||
/* Set LVDS normal mode. */
|
||||
lvdcr0 |= LVDCR0_PWD;
|
||||
rcar_lvds_write(lvds, LVDCR0, lvdcr0);
|
||||
}
|
||||
|
||||
/* Wait for the startup delay. */
|
||||
usleep_range(100, 150);
|
||||
|
||||
/* Turn the output on. */
|
||||
lvdcr0 |= LVDCR0_LVRES;
|
||||
rcar_lvds_write(lvds, LVDCR0, lvdcr0);
|
||||
|
||||
lvds->enabled = true;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void rcar_du_lvdsenc_stop(struct rcar_du_lvdsenc *lvds)
|
||||
{
|
||||
if (!lvds->enabled)
|
||||
return;
|
||||
|
||||
rcar_lvds_write(lvds, LVDCR0, 0);
|
||||
rcar_lvds_write(lvds, LVDCR1, 0);
|
||||
|
||||
clk_disable_unprepare(lvds->clock);
|
||||
|
||||
lvds->enabled = false;
|
||||
}
|
||||
|
||||
int rcar_du_lvdsenc_enable(struct rcar_du_lvdsenc *lvds, struct drm_crtc *crtc,
|
||||
bool enable)
|
||||
{
|
||||
if (!enable) {
|
||||
rcar_du_lvdsenc_stop(lvds);
|
||||
return 0;
|
||||
} else if (crtc) {
|
||||
struct rcar_du_crtc *rcrtc = to_rcar_crtc(crtc);
|
||||
return rcar_du_lvdsenc_start(lvds, rcrtc);
|
||||
} else
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
void rcar_du_lvdsenc_atomic_check(struct rcar_du_lvdsenc *lvds,
|
||||
struct drm_display_mode *mode)
|
||||
{
|
||||
/*
|
||||
* The internal LVDS encoder has a restricted clock frequency operating
|
||||
* range (31MHz to 148.5MHz). Clamp the clock accordingly.
|
||||
*/
|
||||
mode->clock = clamp(mode->clock, 31000, 148500);
|
||||
}
|
||||
|
||||
void rcar_du_lvdsenc_set_mode(struct rcar_du_lvdsenc *lvds,
|
||||
enum rcar_lvds_mode mode)
|
||||
{
|
||||
lvds->mode = mode;
|
||||
}
|
||||
|
||||
static int rcar_du_lvdsenc_get_resources(struct rcar_du_lvdsenc *lvds,
|
||||
struct platform_device *pdev)
|
||||
{
|
||||
struct resource *mem;
|
||||
char name[7];
|
||||
|
||||
sprintf(name, "lvds.%u", lvds->index);
|
||||
|
||||
mem = platform_get_resource_byname(pdev, IORESOURCE_MEM, name);
|
||||
lvds->mmio = devm_ioremap_resource(&pdev->dev, mem);
|
||||
if (IS_ERR(lvds->mmio))
|
||||
return PTR_ERR(lvds->mmio);
|
||||
|
||||
lvds->clock = devm_clk_get(&pdev->dev, name);
|
||||
if (IS_ERR(lvds->clock)) {
|
||||
dev_err(&pdev->dev, "failed to get clock for %s\n", name);
|
||||
return PTR_ERR(lvds->clock);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int rcar_du_lvdsenc_init(struct rcar_du_device *rcdu)
|
||||
{
|
||||
struct platform_device *pdev = to_platform_device(rcdu->dev);
|
||||
struct rcar_du_lvdsenc *lvds;
|
||||
unsigned int i;
|
||||
int ret;
|
||||
|
||||
for (i = 0; i < rcdu->info->num_lvds; ++i) {
|
||||
lvds = devm_kzalloc(&pdev->dev, sizeof(*lvds), GFP_KERNEL);
|
||||
if (lvds == NULL)
|
||||
return -ENOMEM;
|
||||
|
||||
lvds->dev = rcdu;
|
||||
lvds->index = i;
|
||||
lvds->input = i ? RCAR_LVDS_INPUT_DU1 : RCAR_LVDS_INPUT_DU0;
|
||||
lvds->enabled = false;
|
||||
|
||||
ret = rcar_du_lvdsenc_get_resources(lvds, pdev);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
|
||||
rcdu->lvds[i] = lvds;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
/*
|
||||
* rcar_du_lvdsenc.h -- R-Car Display Unit LVDS Encoder
|
||||
*
|
||||
* Copyright (C) 2013-2014 Renesas Electronics Corporation
|
||||
*
|
||||
* Contact: Laurent Pinchart (laurent.pinchart@ideasonboard.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
#ifndef __RCAR_DU_LVDSENC_H__
|
||||
#define __RCAR_DU_LVDSENC_H__
|
||||
|
||||
#include <linux/io.h>
|
||||
#include <linux/module.h>
|
||||
|
||||
struct rcar_drm_crtc;
|
||||
struct rcar_du_lvdsenc;
|
||||
|
||||
enum rcar_lvds_input {
|
||||
RCAR_LVDS_INPUT_DU0,
|
||||
RCAR_LVDS_INPUT_DU1,
|
||||
RCAR_LVDS_INPUT_DU2,
|
||||
};
|
||||
|
||||
/* Keep in sync with the LVDCR0.LVMD hardware register values. */
|
||||
enum rcar_lvds_mode {
|
||||
RCAR_LVDS_MODE_JEIDA = 0,
|
||||
RCAR_LVDS_MODE_MIRROR = 1,
|
||||
RCAR_LVDS_MODE_VESA = 4,
|
||||
};
|
||||
|
||||
#if IS_ENABLED(CONFIG_DRM_RCAR_LVDS)
|
||||
int rcar_du_lvdsenc_init(struct rcar_du_device *rcdu);
|
||||
void rcar_du_lvdsenc_set_mode(struct rcar_du_lvdsenc *lvds,
|
||||
enum rcar_lvds_mode mode);
|
||||
int rcar_du_lvdsenc_enable(struct rcar_du_lvdsenc *lvds,
|
||||
struct drm_crtc *crtc, bool enable);
|
||||
void rcar_du_lvdsenc_atomic_check(struct rcar_du_lvdsenc *lvds,
|
||||
struct drm_display_mode *mode);
|
||||
#else
|
||||
static inline int rcar_du_lvdsenc_init(struct rcar_du_device *rcdu)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
static inline void rcar_du_lvdsenc_set_mode(struct rcar_du_lvdsenc *lvds,
|
||||
enum rcar_lvds_mode mode)
|
||||
{
|
||||
}
|
||||
static inline int rcar_du_lvdsenc_enable(struct rcar_du_lvdsenc *lvds,
|
||||
struct drm_crtc *crtc, bool enable)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
static inline void rcar_du_lvdsenc_atomic_check(struct rcar_du_lvdsenc *lvds,
|
||||
struct drm_display_mode *mode)
|
||||
{
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __RCAR_DU_LVDSENC_H__ */
|
|
@ -0,0 +1,524 @@
|
|||
// SPDX-License-Identifier: GPL-2.0
|
||||
/*
|
||||
* rcar_lvds.c -- R-Car LVDS Encoder
|
||||
*
|
||||
* Copyright (C) 2013-2018 Renesas Electronics Corporation
|
||||
*
|
||||
* Contact: Laurent Pinchart (laurent.pinchart@ideasonboard.com)
|
||||
*/
|
||||
|
||||
#include <linux/clk.h>
|
||||
#include <linux/delay.h>
|
||||
#include <linux/io.h>
|
||||
#include <linux/of.h>
|
||||
#include <linux/of_device.h>
|
||||
#include <linux/of_graph.h>
|
||||
#include <linux/platform_device.h>
|
||||
#include <linux/slab.h>
|
||||
|
||||
#include <drm/drm_atomic.h>
|
||||
#include <drm/drm_atomic_helper.h>
|
||||
#include <drm/drm_bridge.h>
|
||||
#include <drm/drm_crtc_helper.h>
|
||||
#include <drm/drm_panel.h>
|
||||
|
||||
#include "rcar_lvds_regs.h"
|
||||
|
||||
/* Keep in sync with the LVDCR0.LVMD hardware register values. */
|
||||
enum rcar_lvds_mode {
|
||||
RCAR_LVDS_MODE_JEIDA = 0,
|
||||
RCAR_LVDS_MODE_MIRROR = 1,
|
||||
RCAR_LVDS_MODE_VESA = 4,
|
||||
};
|
||||
|
||||
#define RCAR_LVDS_QUIRK_LANES (1 << 0) /* LVDS lanes 1 and 3 inverted */
|
||||
|
||||
struct rcar_lvds_device_info {
|
||||
unsigned int gen;
|
||||
unsigned int quirks;
|
||||
};
|
||||
|
||||
struct rcar_lvds {
|
||||
struct device *dev;
|
||||
const struct rcar_lvds_device_info *info;
|
||||
|
||||
struct drm_bridge bridge;
|
||||
|
||||
struct drm_bridge *next_bridge;
|
||||
struct drm_connector connector;
|
||||
struct drm_panel *panel;
|
||||
|
||||
void __iomem *mmio;
|
||||
struct clk *clock;
|
||||
bool enabled;
|
||||
|
||||
struct drm_display_mode display_mode;
|
||||
enum rcar_lvds_mode mode;
|
||||
};
|
||||
|
||||
#define bridge_to_rcar_lvds(bridge) \
|
||||
container_of(bridge, struct rcar_lvds, bridge)
|
||||
|
||||
#define connector_to_rcar_lvds(connector) \
|
||||
container_of(connector, struct rcar_lvds, connector)
|
||||
|
||||
static void rcar_lvds_write(struct rcar_lvds *lvds, u32 reg, u32 data)
|
||||
{
|
||||
iowrite32(data, lvds->mmio + reg);
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* Connector & Panel
|
||||
*/
|
||||
|
||||
static int rcar_lvds_connector_get_modes(struct drm_connector *connector)
|
||||
{
|
||||
struct rcar_lvds *lvds = connector_to_rcar_lvds(connector);
|
||||
|
||||
return drm_panel_get_modes(lvds->panel);
|
||||
}
|
||||
|
||||
static int rcar_lvds_connector_atomic_check(struct drm_connector *connector,
|
||||
struct drm_connector_state *state)
|
||||
{
|
||||
struct rcar_lvds *lvds = connector_to_rcar_lvds(connector);
|
||||
const struct drm_display_mode *panel_mode;
|
||||
struct drm_crtc_state *crtc_state;
|
||||
|
||||
if (list_empty(&connector->modes)) {
|
||||
dev_dbg(lvds->dev, "connector: empty modes list\n");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
panel_mode = list_first_entry(&connector->modes,
|
||||
struct drm_display_mode, head);
|
||||
|
||||
/* We're not allowed to modify the resolution. */
|
||||
crtc_state = drm_atomic_get_crtc_state(state->state, state->crtc);
|
||||
if (IS_ERR(crtc_state))
|
||||
return PTR_ERR(crtc_state);
|
||||
|
||||
if (crtc_state->mode.hdisplay != panel_mode->hdisplay ||
|
||||
crtc_state->mode.vdisplay != panel_mode->vdisplay)
|
||||
return -EINVAL;
|
||||
|
||||
/* The flat panel mode is fixed, just copy it to the adjusted mode. */
|
||||
drm_mode_copy(&crtc_state->adjusted_mode, panel_mode);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const struct drm_connector_helper_funcs rcar_lvds_conn_helper_funcs = {
|
||||
.get_modes = rcar_lvds_connector_get_modes,
|
||||
.atomic_check = rcar_lvds_connector_atomic_check,
|
||||
};
|
||||
|
||||
static const struct drm_connector_funcs rcar_lvds_conn_funcs = {
|
||||
.reset = drm_atomic_helper_connector_reset,
|
||||
.fill_modes = drm_helper_probe_single_connector_modes,
|
||||
.destroy = drm_connector_cleanup,
|
||||
.atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state,
|
||||
.atomic_destroy_state = drm_atomic_helper_connector_destroy_state,
|
||||
};
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* Bridge
|
||||
*/
|
||||
|
||||
static u32 rcar_lvds_lvdpllcr_gen2(unsigned int freq)
|
||||
{
|
||||
if (freq < 39000)
|
||||
return LVDPLLCR_CEEN | LVDPLLCR_COSEL | LVDPLLCR_PLLDLYCNT_38M;
|
||||
else if (freq < 61000)
|
||||
return LVDPLLCR_CEEN | LVDPLLCR_COSEL | LVDPLLCR_PLLDLYCNT_60M;
|
||||
else if (freq < 121000)
|
||||
return LVDPLLCR_CEEN | LVDPLLCR_COSEL | LVDPLLCR_PLLDLYCNT_121M;
|
||||
else
|
||||
return LVDPLLCR_PLLDLYCNT_150M;
|
||||
}
|
||||
|
||||
static u32 rcar_lvds_lvdpllcr_gen3(unsigned int freq)
|
||||
{
|
||||
if (freq < 42000)
|
||||
return LVDPLLCR_PLLDIVCNT_42M;
|
||||
else if (freq < 85000)
|
||||
return LVDPLLCR_PLLDIVCNT_85M;
|
||||
else if (freq < 128000)
|
||||
return LVDPLLCR_PLLDIVCNT_128M;
|
||||
else
|
||||
return LVDPLLCR_PLLDIVCNT_148M;
|
||||
}
|
||||
|
||||
static void rcar_lvds_enable(struct drm_bridge *bridge)
|
||||
{
|
||||
struct rcar_lvds *lvds = bridge_to_rcar_lvds(bridge);
|
||||
const struct drm_display_mode *mode = &lvds->display_mode;
|
||||
/*
|
||||
* FIXME: We should really retrieve the CRTC through the state, but how
|
||||
* do we get a state pointer?
|
||||
*/
|
||||
struct drm_crtc *crtc = lvds->bridge.encoder->crtc;
|
||||
u32 lvdpllcr;
|
||||
u32 lvdhcr;
|
||||
u32 lvdcr0;
|
||||
int ret;
|
||||
|
||||
WARN_ON(lvds->enabled);
|
||||
|
||||
ret = clk_prepare_enable(lvds->clock);
|
||||
if (ret < 0)
|
||||
return;
|
||||
|
||||
/*
|
||||
* Hardcode the channels and control signals routing for now.
|
||||
*
|
||||
* HSYNC -> CTRL0
|
||||
* VSYNC -> CTRL1
|
||||
* DISP -> CTRL2
|
||||
* 0 -> CTRL3
|
||||
*/
|
||||
rcar_lvds_write(lvds, LVDCTRCR, LVDCTRCR_CTR3SEL_ZERO |
|
||||
LVDCTRCR_CTR2SEL_DISP | LVDCTRCR_CTR1SEL_VSYNC |
|
||||
LVDCTRCR_CTR0SEL_HSYNC);
|
||||
|
||||
if (lvds->info->quirks & RCAR_LVDS_QUIRK_LANES)
|
||||
lvdhcr = LVDCHCR_CHSEL_CH(0, 0) | LVDCHCR_CHSEL_CH(1, 3)
|
||||
| LVDCHCR_CHSEL_CH(2, 2) | LVDCHCR_CHSEL_CH(3, 1);
|
||||
else
|
||||
lvdhcr = LVDCHCR_CHSEL_CH(0, 0) | LVDCHCR_CHSEL_CH(1, 1)
|
||||
| LVDCHCR_CHSEL_CH(2, 2) | LVDCHCR_CHSEL_CH(3, 3);
|
||||
|
||||
rcar_lvds_write(lvds, LVDCHCR, lvdhcr);
|
||||
|
||||
/* PLL clock configuration. */
|
||||
if (lvds->info->gen < 3)
|
||||
lvdpllcr = rcar_lvds_lvdpllcr_gen2(mode->clock);
|
||||
else
|
||||
lvdpllcr = rcar_lvds_lvdpllcr_gen3(mode->clock);
|
||||
rcar_lvds_write(lvds, LVDPLLCR, lvdpllcr);
|
||||
|
||||
/* Set the LVDS mode and select the input. */
|
||||
lvdcr0 = lvds->mode << LVDCR0_LVMD_SHIFT;
|
||||
if (drm_crtc_index(crtc) == 2)
|
||||
lvdcr0 |= LVDCR0_DUSEL;
|
||||
rcar_lvds_write(lvds, LVDCR0, lvdcr0);
|
||||
|
||||
/* Turn all the channels on. */
|
||||
rcar_lvds_write(lvds, LVDCR1,
|
||||
LVDCR1_CHSTBY(3) | LVDCR1_CHSTBY(2) |
|
||||
LVDCR1_CHSTBY(1) | LVDCR1_CHSTBY(0) | LVDCR1_CLKSTBY);
|
||||
|
||||
if (lvds->info->gen < 3) {
|
||||
/* Enable LVDS operation and turn the bias circuitry on. */
|
||||
lvdcr0 |= LVDCR0_BEN | LVDCR0_LVEN;
|
||||
rcar_lvds_write(lvds, LVDCR0, lvdcr0);
|
||||
}
|
||||
|
||||
/* Turn the PLL on. */
|
||||
lvdcr0 |= LVDCR0_PLLON;
|
||||
rcar_lvds_write(lvds, LVDCR0, lvdcr0);
|
||||
|
||||
if (lvds->info->gen > 2) {
|
||||
/* Set LVDS normal mode. */
|
||||
lvdcr0 |= LVDCR0_PWD;
|
||||
rcar_lvds_write(lvds, LVDCR0, lvdcr0);
|
||||
}
|
||||
|
||||
/* Wait for the startup delay. */
|
||||
usleep_range(100, 150);
|
||||
|
||||
/* Turn the output on. */
|
||||
lvdcr0 |= LVDCR0_LVRES;
|
||||
rcar_lvds_write(lvds, LVDCR0, lvdcr0);
|
||||
|
||||
if (lvds->panel) {
|
||||
drm_panel_prepare(lvds->panel);
|
||||
drm_panel_enable(lvds->panel);
|
||||
}
|
||||
|
||||
lvds->enabled = true;
|
||||
}
|
||||
|
||||
static void rcar_lvds_disable(struct drm_bridge *bridge)
|
||||
{
|
||||
struct rcar_lvds *lvds = bridge_to_rcar_lvds(bridge);
|
||||
|
||||
WARN_ON(!lvds->enabled);
|
||||
|
||||
if (lvds->panel) {
|
||||
drm_panel_disable(lvds->panel);
|
||||
drm_panel_unprepare(lvds->panel);
|
||||
}
|
||||
|
||||
rcar_lvds_write(lvds, LVDCR0, 0);
|
||||
rcar_lvds_write(lvds, LVDCR1, 0);
|
||||
|
||||
clk_disable_unprepare(lvds->clock);
|
||||
|
||||
lvds->enabled = false;
|
||||
}
|
||||
|
||||
static bool rcar_lvds_mode_fixup(struct drm_bridge *bridge,
|
||||
const struct drm_display_mode *mode,
|
||||
struct drm_display_mode *adjusted_mode)
|
||||
{
|
||||
/*
|
||||
* The internal LVDS encoder has a restricted clock frequency operating
|
||||
* range (31MHz to 148.5MHz). Clamp the clock accordingly.
|
||||
*/
|
||||
adjusted_mode->clock = clamp(adjusted_mode->clock, 31000, 148500);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void rcar_lvds_get_lvds_mode(struct rcar_lvds *lvds)
|
||||
{
|
||||
struct drm_display_info *info = &lvds->connector.display_info;
|
||||
enum rcar_lvds_mode mode;
|
||||
|
||||
/*
|
||||
* There is no API yet to retrieve LVDS mode from a bridge, only panels
|
||||
* are supported.
|
||||
*/
|
||||
if (!lvds->panel)
|
||||
return;
|
||||
|
||||
if (!info->num_bus_formats || !info->bus_formats) {
|
||||
dev_err(lvds->dev, "no LVDS bus format reported\n");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (info->bus_formats[0]) {
|
||||
case MEDIA_BUS_FMT_RGB666_1X7X3_SPWG:
|
||||
case MEDIA_BUS_FMT_RGB888_1X7X4_JEIDA:
|
||||
mode = RCAR_LVDS_MODE_JEIDA;
|
||||
break;
|
||||
case MEDIA_BUS_FMT_RGB888_1X7X4_SPWG:
|
||||
mode = RCAR_LVDS_MODE_VESA;
|
||||
break;
|
||||
default:
|
||||
dev_err(lvds->dev, "unsupported LVDS bus format 0x%04x\n",
|
||||
info->bus_formats[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (info->bus_flags & DRM_BUS_FLAG_DATA_LSB_TO_MSB)
|
||||
mode |= RCAR_LVDS_MODE_MIRROR;
|
||||
|
||||
lvds->mode = mode;
|
||||
}
|
||||
|
||||
static void rcar_lvds_mode_set(struct drm_bridge *bridge,
|
||||
struct drm_display_mode *mode,
|
||||
struct drm_display_mode *adjusted_mode)
|
||||
{
|
||||
struct rcar_lvds *lvds = bridge_to_rcar_lvds(bridge);
|
||||
|
||||
WARN_ON(lvds->enabled);
|
||||
|
||||
lvds->display_mode = *adjusted_mode;
|
||||
|
||||
rcar_lvds_get_lvds_mode(lvds);
|
||||
}
|
||||
|
||||
static int rcar_lvds_attach(struct drm_bridge *bridge)
|
||||
{
|
||||
struct rcar_lvds *lvds = bridge_to_rcar_lvds(bridge);
|
||||
struct drm_connector *connector = &lvds->connector;
|
||||
struct drm_encoder *encoder = bridge->encoder;
|
||||
int ret;
|
||||
|
||||
/* If we have a next bridge just attach it. */
|
||||
if (lvds->next_bridge)
|
||||
return drm_bridge_attach(bridge->encoder, lvds->next_bridge,
|
||||
bridge);
|
||||
|
||||
/* Otherwise we have a panel, create a connector. */
|
||||
ret = drm_connector_init(bridge->dev, connector, &rcar_lvds_conn_funcs,
|
||||
DRM_MODE_CONNECTOR_LVDS);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
|
||||
drm_connector_helper_add(connector, &rcar_lvds_conn_helper_funcs);
|
||||
|
||||
ret = drm_mode_connector_attach_encoder(connector, encoder);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
|
||||
return drm_panel_attach(lvds->panel, connector);
|
||||
}
|
||||
|
||||
static void rcar_lvds_detach(struct drm_bridge *bridge)
|
||||
{
|
||||
struct rcar_lvds *lvds = bridge_to_rcar_lvds(bridge);
|
||||
|
||||
if (lvds->panel)
|
||||
drm_panel_detach(lvds->panel);
|
||||
}
|
||||
|
||||
static const struct drm_bridge_funcs rcar_lvds_bridge_ops = {
|
||||
.attach = rcar_lvds_attach,
|
||||
.detach = rcar_lvds_detach,
|
||||
.enable = rcar_lvds_enable,
|
||||
.disable = rcar_lvds_disable,
|
||||
.mode_fixup = rcar_lvds_mode_fixup,
|
||||
.mode_set = rcar_lvds_mode_set,
|
||||
};
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* Probe & Remove
|
||||
*/
|
||||
|
||||
static int rcar_lvds_parse_dt(struct rcar_lvds *lvds)
|
||||
{
|
||||
struct device_node *local_output = NULL;
|
||||
struct device_node *remote_input = NULL;
|
||||
struct device_node *remote = NULL;
|
||||
struct device_node *node;
|
||||
bool is_bridge = false;
|
||||
int ret = 0;
|
||||
|
||||
local_output = of_graph_get_endpoint_by_regs(lvds->dev->of_node, 1, 0);
|
||||
if (!local_output) {
|
||||
dev_dbg(lvds->dev, "unconnected port@1\n");
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
/*
|
||||
* Locate the connected entity and infer its type from the number of
|
||||
* endpoints.
|
||||
*/
|
||||
remote = of_graph_get_remote_port_parent(local_output);
|
||||
if (!remote) {
|
||||
dev_dbg(lvds->dev, "unconnected endpoint %pOF\n", local_output);
|
||||
ret = -ENODEV;
|
||||
goto done;
|
||||
}
|
||||
|
||||
if (!of_device_is_available(remote)) {
|
||||
dev_dbg(lvds->dev, "connected entity %pOF is disabled\n",
|
||||
remote);
|
||||
ret = -ENODEV;
|
||||
goto done;
|
||||
}
|
||||
|
||||
remote_input = of_graph_get_remote_endpoint(local_output);
|
||||
|
||||
for_each_endpoint_of_node(remote, node) {
|
||||
if (node != remote_input) {
|
||||
/*
|
||||
* We've found one endpoint other than the input, this
|
||||
* must be a bridge.
|
||||
*/
|
||||
is_bridge = true;
|
||||
of_node_put(node);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_bridge) {
|
||||
lvds->next_bridge = of_drm_find_bridge(remote);
|
||||
if (!lvds->next_bridge)
|
||||
ret = -EPROBE_DEFER;
|
||||
} else {
|
||||
lvds->panel = of_drm_find_panel(remote);
|
||||
if (!lvds->panel)
|
||||
ret = -EPROBE_DEFER;
|
||||
}
|
||||
|
||||
done:
|
||||
of_node_put(local_output);
|
||||
of_node_put(remote_input);
|
||||
of_node_put(remote);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int rcar_lvds_probe(struct platform_device *pdev)
|
||||
{
|
||||
struct rcar_lvds *lvds;
|
||||
struct resource *mem;
|
||||
int ret;
|
||||
|
||||
lvds = devm_kzalloc(&pdev->dev, sizeof(*lvds), GFP_KERNEL);
|
||||
if (lvds == NULL)
|
||||
return -ENOMEM;
|
||||
|
||||
platform_set_drvdata(pdev, lvds);
|
||||
|
||||
lvds->dev = &pdev->dev;
|
||||
lvds->info = of_device_get_match_data(&pdev->dev);
|
||||
lvds->enabled = false;
|
||||
|
||||
ret = rcar_lvds_parse_dt(lvds);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
|
||||
lvds->bridge.driver_private = lvds;
|
||||
lvds->bridge.funcs = &rcar_lvds_bridge_ops;
|
||||
lvds->bridge.of_node = pdev->dev.of_node;
|
||||
|
||||
mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
|
||||
lvds->mmio = devm_ioremap_resource(&pdev->dev, mem);
|
||||
if (IS_ERR(lvds->mmio))
|
||||
return PTR_ERR(lvds->mmio);
|
||||
|
||||
lvds->clock = devm_clk_get(&pdev->dev, NULL);
|
||||
if (IS_ERR(lvds->clock)) {
|
||||
dev_err(&pdev->dev, "failed to get clock\n");
|
||||
return PTR_ERR(lvds->clock);
|
||||
}
|
||||
|
||||
drm_bridge_add(&lvds->bridge);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int rcar_lvds_remove(struct platform_device *pdev)
|
||||
{
|
||||
struct rcar_lvds *lvds = platform_get_drvdata(pdev);
|
||||
|
||||
drm_bridge_remove(&lvds->bridge);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const struct rcar_lvds_device_info rcar_lvds_gen2_info = {
|
||||
.gen = 2,
|
||||
};
|
||||
|
||||
static const struct rcar_lvds_device_info rcar_lvds_r8a7790_info = {
|
||||
.gen = 2,
|
||||
.quirks = RCAR_LVDS_QUIRK_LANES,
|
||||
};
|
||||
|
||||
static const struct rcar_lvds_device_info rcar_lvds_gen3_info = {
|
||||
.gen = 3,
|
||||
};
|
||||
|
||||
static const struct of_device_id rcar_lvds_of_table[] = {
|
||||
{ .compatible = "renesas,r8a7743-lvds", .data = &rcar_lvds_gen2_info },
|
||||
{ .compatible = "renesas,r8a7790-lvds", .data = &rcar_lvds_r8a7790_info },
|
||||
{ .compatible = "renesas,r8a7791-lvds", .data = &rcar_lvds_gen2_info },
|
||||
{ .compatible = "renesas,r8a7793-lvds", .data = &rcar_lvds_gen2_info },
|
||||
{ .compatible = "renesas,r8a7795-lvds", .data = &rcar_lvds_gen3_info },
|
||||
{ .compatible = "renesas,r8a7796-lvds", .data = &rcar_lvds_gen3_info },
|
||||
{ }
|
||||
};
|
||||
|
||||
MODULE_DEVICE_TABLE(of, rcar_lvds_of_table);
|
||||
|
||||
static struct platform_driver rcar_lvds_platform_driver = {
|
||||
.probe = rcar_lvds_probe,
|
||||
.remove = rcar_lvds_remove,
|
||||
.driver = {
|
||||
.name = "rcar-lvds",
|
||||
.of_match_table = rcar_lvds_of_table,
|
||||
},
|
||||
};
|
||||
|
||||
module_platform_driver(rcar_lvds_platform_driver);
|
||||
|
||||
MODULE_AUTHOR("Laurent Pinchart <laurent.pinchart@ideasonboard.com>");
|
||||
MODULE_DESCRIPTION("Renesas R-Car LVDS Encoder Driver");
|
||||
MODULE_LICENSE("GPL");
|
Loading…
Reference in New Issue