2019-05-27 08:55:01 +02:00
|
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
2017-05-27 18:09:35 +02:00
|
|
|
/*
|
|
|
|
* Copyright (C) 2016 Maxime Ripard
|
|
|
|
*
|
|
|
|
* Maxime Ripard <maxime.ripard@free-electrons.com>
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include <linux/clk.h>
|
|
|
|
#include <linux/component.h>
|
2022-06-30 22:51:14 +03:00
|
|
|
#include <linux/i2c.h>
|
2017-05-27 18:09:35 +02:00
|
|
|
#include <linux/iopoll.h>
|
2019-07-16 08:42:06 +02:00
|
|
|
#include <linux/module.h>
|
2023-07-14 11:45:34 -06:00
|
|
|
#include <linux/of.h>
|
2017-05-27 18:09:35 +02:00
|
|
|
#include <linux/platform_device.h>
|
|
|
|
#include <linux/pm_runtime.h>
|
2017-10-10 11:20:01 +08:00
|
|
|
#include <linux/regmap.h>
|
drm/sun4i: hdmi: Add support for controller hardware variants
The HDMI controller found in earlier Allwinner SoCs have slight
differences between the A10, A10s, and the A31:
- Need different initial values for the PLL related registers
- Different behavior of the DDC and TMDS clocks
- Different register layout for the DDC portion
- Separate DDC parent clock on the A31
- Explicit reset control
For the A31, the HDMI TMDS clock has a different value offset for
the divider. The HDMI DDC block is different from the one in the
other SoCs. As far as the DDC clock goes, it has no pre-divider,
as it is clocked from a slower parent clock, not the TMDS clock.
The divider offset from the register value is different. And the
clock control register is at a different offset.
A new variant data structure is created to store pointers to the
above functions, structures, and the different initial values.
Another flag notates whether there is a separate DDC parent clock.
If not, the TMDS clock is passed to the DDC clock create function,
as before.
Regmap fields are used to deal with the different register layout
of the DDC block.
Signed-off-by: Chen-Yu Tsai <wens@csie.org>
Acked-by: Maxime Ripard <maxime.ripard@free-electrons.com>
Signed-off-by: Maxime Ripard <maxime.ripard@free-electrons.com>
Link: https://patchwork.freedesktop.org/patch/msgid/20171010032008.682-8-wens@csie.org
2017-10-10 11:20:04 +08:00
|
|
|
#include <linux/reset.h>
|
2017-05-27 18:09:35 +02:00
|
|
|
|
2024-03-01 09:02:49 +01:00
|
|
|
#include <drm/drm_atomic.h>
|
2019-07-16 08:42:06 +02:00
|
|
|
#include <drm/drm_atomic_helper.h>
|
|
|
|
#include <drm/drm_edid.h>
|
|
|
|
#include <drm/drm_encoder.h>
|
|
|
|
#include <drm/drm_of.h>
|
|
|
|
#include <drm/drm_panel.h>
|
|
|
|
#include <drm/drm_print.h>
|
|
|
|
#include <drm/drm_probe_helper.h>
|
2020-03-05 16:59:42 +01:00
|
|
|
#include <drm/drm_simple_kms_helper.h>
|
2019-07-16 08:42:06 +02:00
|
|
|
|
2024-05-27 15:58:18 +02:00
|
|
|
#include <drm/display/drm_hdmi_helper.h>
|
|
|
|
#include <drm/display/drm_hdmi_state_helper.h>
|
|
|
|
|
2017-05-27 18:09:35 +02:00
|
|
|
#include "sun4i_backend.h"
|
|
|
|
#include "sun4i_crtc.h"
|
|
|
|
#include "sun4i_drv.h"
|
|
|
|
#include "sun4i_hdmi.h"
|
|
|
|
|
2024-02-22 19:14:20 +01:00
|
|
|
#define drm_encoder_to_sun4i_hdmi(e) \
|
|
|
|
container_of_const(e, struct sun4i_hdmi, encoder)
|
2017-05-27 18:09:35 +02:00
|
|
|
|
2024-02-22 19:14:20 +01:00
|
|
|
#define drm_connector_to_sun4i_hdmi(c) \
|
|
|
|
container_of_const(c, struct sun4i_hdmi, connector)
|
2017-05-27 18:09:35 +02:00
|
|
|
|
2024-05-27 15:58:18 +02:00
|
|
|
static int sun4i_hdmi_write_infoframe(struct drm_connector *connector,
|
|
|
|
enum hdmi_infoframe_type type,
|
|
|
|
const u8 *buffer, size_t len)
|
2017-05-27 18:09:35 +02:00
|
|
|
{
|
2024-05-27 15:58:18 +02:00
|
|
|
struct sun4i_hdmi *hdmi = drm_connector_to_sun4i_hdmi(connector);
|
|
|
|
int i;
|
2017-05-27 18:09:35 +02:00
|
|
|
|
2024-05-27 15:58:18 +02:00
|
|
|
if (type != HDMI_INFOFRAME_TYPE_AVI) {
|
|
|
|
drm_err(connector->dev,
|
|
|
|
"Unsupported infoframe type: %u\n", type);
|
|
|
|
return 0;
|
2017-05-27 18:09:35 +02:00
|
|
|
}
|
|
|
|
|
2024-05-27 15:58:18 +02:00
|
|
|
for (i = 0; i < len; i++)
|
2017-05-27 18:09:35 +02:00
|
|
|
writeb(buffer[i], hdmi->base + SUN4I_HDMI_AVI_INFOFRAME_REG(i));
|
|
|
|
|
|
|
|
return 0;
|
2024-05-27 15:58:18 +02:00
|
|
|
|
2017-05-27 18:09:35 +02:00
|
|
|
}
|
|
|
|
|
2024-02-22 19:14:18 +01:00
|
|
|
static void sun4i_hdmi_disable(struct drm_encoder *encoder,
|
|
|
|
struct drm_atomic_state *state)
|
2017-05-27 18:09:35 +02:00
|
|
|
{
|
|
|
|
struct sun4i_hdmi *hdmi = drm_encoder_to_sun4i_hdmi(encoder);
|
|
|
|
u32 val;
|
|
|
|
|
|
|
|
DRM_DEBUG_DRIVER("Disabling the HDMI Output\n");
|
|
|
|
|
|
|
|
val = readl(hdmi->base + SUN4I_HDMI_VID_CTRL_REG);
|
|
|
|
val &= ~SUN4I_HDMI_VID_CTRL_ENABLE;
|
|
|
|
writel(val, hdmi->base + SUN4I_HDMI_VID_CTRL_REG);
|
2019-01-22 09:32:32 +02:00
|
|
|
|
|
|
|
clk_disable_unprepare(hdmi->tmds_clk);
|
2017-05-27 18:09:35 +02:00
|
|
|
}
|
|
|
|
|
2024-02-22 19:14:18 +01:00
|
|
|
static void sun4i_hdmi_enable(struct drm_encoder *encoder,
|
|
|
|
struct drm_atomic_state *state)
|
2017-05-27 18:09:35 +02:00
|
|
|
{
|
|
|
|
struct drm_display_mode *mode = &encoder->crtc->state->adjusted_mode;
|
|
|
|
struct sun4i_hdmi *hdmi = drm_encoder_to_sun4i_hdmi(encoder);
|
2024-05-27 15:58:18 +02:00
|
|
|
struct drm_connector *connector = &hdmi->connector;
|
|
|
|
struct drm_display_info *display = &connector->display_info;
|
|
|
|
struct drm_connector_state *conn_state =
|
|
|
|
drm_atomic_get_new_connector_state(state, connector);
|
|
|
|
unsigned long long tmds_rate = conn_state->hdmi.tmds_char_rate;
|
2024-02-22 19:14:19 +01:00
|
|
|
unsigned int x, y;
|
2017-05-27 18:09:35 +02:00
|
|
|
u32 val = 0;
|
|
|
|
|
|
|
|
DRM_DEBUG_DRIVER("Enabling the HDMI Output\n");
|
|
|
|
|
2024-05-27 15:58:18 +02:00
|
|
|
clk_set_rate(hdmi->mod_clk, tmds_rate);
|
|
|
|
clk_set_rate(hdmi->tmds_clk, tmds_rate);
|
2017-05-27 18:09:35 +02:00
|
|
|
|
|
|
|
/* Set input sync enable */
|
|
|
|
writel(SUN4I_HDMI_UNKNOWN_INPUT_SYNC,
|
|
|
|
hdmi->base + SUN4I_HDMI_UNKNOWN_REG);
|
|
|
|
|
2017-10-14 12:02:52 +08:00
|
|
|
/*
|
|
|
|
* Setup output pad (?) controls
|
|
|
|
*
|
|
|
|
* This is done here instead of at probe/bind time because
|
|
|
|
* the controller seems to toggle some of the bits on its own.
|
|
|
|
*
|
|
|
|
* We can't just initialize the register there, we need to
|
|
|
|
* protect the clock bits that have already been read out and
|
|
|
|
* cached by the clock framework.
|
|
|
|
*/
|
|
|
|
val = readl(hdmi->base + SUN4I_HDMI_PAD_CTRL1_REG);
|
|
|
|
val &= SUN4I_HDMI_PAD_CTRL1_HALVE_CLK;
|
|
|
|
val |= hdmi->variant->pad_ctrl1_init_val;
|
|
|
|
writel(val, hdmi->base + SUN4I_HDMI_PAD_CTRL1_REG);
|
|
|
|
val = readl(hdmi->base + SUN4I_HDMI_PAD_CTRL1_REG);
|
|
|
|
|
2017-05-27 18:09:35 +02:00
|
|
|
/* Setup timing registers */
|
|
|
|
writel(SUN4I_HDMI_VID_TIMING_X(mode->hdisplay) |
|
|
|
|
SUN4I_HDMI_VID_TIMING_Y(mode->vdisplay),
|
|
|
|
hdmi->base + SUN4I_HDMI_VID_TIMING_ACT_REG);
|
|
|
|
|
|
|
|
x = mode->htotal - mode->hsync_start;
|
|
|
|
y = mode->vtotal - mode->vsync_start;
|
|
|
|
writel(SUN4I_HDMI_VID_TIMING_X(x) | SUN4I_HDMI_VID_TIMING_Y(y),
|
|
|
|
hdmi->base + SUN4I_HDMI_VID_TIMING_BP_REG);
|
|
|
|
|
|
|
|
x = mode->hsync_start - mode->hdisplay;
|
|
|
|
y = mode->vsync_start - mode->vdisplay;
|
|
|
|
writel(SUN4I_HDMI_VID_TIMING_X(x) | SUN4I_HDMI_VID_TIMING_Y(y),
|
|
|
|
hdmi->base + SUN4I_HDMI_VID_TIMING_FP_REG);
|
|
|
|
|
|
|
|
x = mode->hsync_end - mode->hsync_start;
|
|
|
|
y = mode->vsync_end - mode->vsync_start;
|
|
|
|
writel(SUN4I_HDMI_VID_TIMING_X(x) | SUN4I_HDMI_VID_TIMING_Y(y),
|
|
|
|
hdmi->base + SUN4I_HDMI_VID_TIMING_SPW_REG);
|
|
|
|
|
|
|
|
val = SUN4I_HDMI_VID_TIMING_POL_TX_CLK;
|
|
|
|
if (mode->flags & DRM_MODE_FLAG_PHSYNC)
|
|
|
|
val |= SUN4I_HDMI_VID_TIMING_POL_HSYNC;
|
|
|
|
|
|
|
|
if (mode->flags & DRM_MODE_FLAG_PVSYNC)
|
|
|
|
val |= SUN4I_HDMI_VID_TIMING_POL_VSYNC;
|
|
|
|
|
|
|
|
writel(val, hdmi->base + SUN4I_HDMI_VID_TIMING_POL_REG);
|
2024-02-22 19:14:19 +01:00
|
|
|
|
|
|
|
clk_prepare_enable(hdmi->tmds_clk);
|
|
|
|
|
2024-05-27 15:58:18 +02:00
|
|
|
drm_atomic_helper_connector_hdmi_update_infoframes(connector, state);
|
|
|
|
|
2024-02-22 19:14:19 +01:00
|
|
|
val |= SUN4I_HDMI_PKT_CTRL_TYPE(0, SUN4I_HDMI_PKT_AVI);
|
|
|
|
val |= SUN4I_HDMI_PKT_CTRL_TYPE(1, SUN4I_HDMI_PKT_END);
|
|
|
|
writel(val, hdmi->base + SUN4I_HDMI_PKT_CTRL_REG(0));
|
|
|
|
|
|
|
|
val = SUN4I_HDMI_VID_CTRL_ENABLE;
|
|
|
|
if (display->is_hdmi)
|
|
|
|
val |= SUN4I_HDMI_VID_CTRL_HDMI_MODE;
|
|
|
|
|
|
|
|
writel(val, hdmi->base + SUN4I_HDMI_VID_CTRL_REG);
|
2017-05-27 18:09:35 +02:00
|
|
|
}
|
|
|
|
|
2024-02-22 19:14:21 +01:00
|
|
|
static const struct drm_encoder_helper_funcs sun4i_hdmi_helper_funcs = {
|
|
|
|
.atomic_disable = sun4i_hdmi_disable,
|
|
|
|
.atomic_enable = sun4i_hdmi_enable,
|
|
|
|
};
|
|
|
|
|
|
|
|
static enum drm_mode_status
|
|
|
|
sun4i_hdmi_connector_clock_valid(const struct drm_connector *connector,
|
|
|
|
const struct drm_display_mode *mode,
|
|
|
|
unsigned long long clock)
|
2017-12-20 11:52:47 +01:00
|
|
|
{
|
2024-02-22 19:14:21 +01:00
|
|
|
const struct sun4i_hdmi *hdmi = drm_connector_to_sun4i_hdmi(connector);
|
2024-03-04 10:12:25 +01:00
|
|
|
unsigned long diff = div_u64(clock, 200); /* +-0.5% allowed by HDMI spec */
|
2017-12-20 11:52:47 +01:00
|
|
|
long rounded_rate;
|
|
|
|
|
2024-02-22 19:14:21 +01:00
|
|
|
if (mode->flags & DRM_MODE_FLAG_DBLCLK)
|
|
|
|
return MODE_BAD;
|
|
|
|
|
2017-12-20 11:52:47 +01:00
|
|
|
/* 165 MHz is the typical max pixelclock frequency for HDMI <= 1.2 */
|
2024-02-22 19:14:21 +01:00
|
|
|
if (clock > 165000000)
|
2017-12-20 11:52:47 +01:00
|
|
|
return MODE_CLOCK_HIGH;
|
2024-02-22 19:14:21 +01:00
|
|
|
|
|
|
|
rounded_rate = clk_round_rate(hdmi->tmds_clk, clock);
|
2017-12-20 11:52:47 +01:00
|
|
|
if (rounded_rate > 0 &&
|
2024-02-22 19:14:21 +01:00
|
|
|
max_t(unsigned long, rounded_rate, clock) -
|
|
|
|
min_t(unsigned long, rounded_rate, clock) < diff)
|
2017-12-20 11:52:47 +01:00
|
|
|
return MODE_OK;
|
2024-02-22 19:14:21 +01:00
|
|
|
|
2017-12-20 11:52:47 +01:00
|
|
|
return MODE_NOCLOCK;
|
|
|
|
}
|
|
|
|
|
2017-05-27 18:09:35 +02:00
|
|
|
static int sun4i_hdmi_get_modes(struct drm_connector *connector)
|
|
|
|
{
|
|
|
|
struct sun4i_hdmi *hdmi = drm_connector_to_sun4i_hdmi(connector);
|
2024-04-16 16:22:20 +03:00
|
|
|
const struct drm_edid *drm_edid;
|
2017-05-27 18:09:35 +02:00
|
|
|
int ret;
|
|
|
|
|
2024-04-16 16:22:20 +03:00
|
|
|
drm_edid = drm_edid_read_ddc(connector, hdmi->ddc_i2c ?: hdmi->i2c);
|
|
|
|
|
|
|
|
drm_edid_connector_update(connector, drm_edid);
|
|
|
|
cec_s_phys_addr(hdmi->cec_adap,
|
|
|
|
connector->display_info.source_physical_address, false);
|
|
|
|
|
|
|
|
if (!drm_edid)
|
2017-05-27 18:09:35 +02:00
|
|
|
return 0;
|
|
|
|
|
|
|
|
DRM_DEBUG_DRIVER("Monitor is %s monitor\n",
|
2022-04-21 19:07:25 +02:00
|
|
|
connector->display_info.is_hdmi ? "an HDMI" : "a DVI");
|
2017-05-27 18:09:35 +02:00
|
|
|
|
2024-04-16 16:22:20 +03:00
|
|
|
|
|
|
|
ret = drm_edid_connector_add_modes(connector);
|
|
|
|
drm_edid_free(drm_edid);
|
2017-05-27 18:09:35 +02:00
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
2019-03-28 13:02:49 +00:00
|
|
|
static struct i2c_adapter *sun4i_hdmi_get_ddc(struct device *dev)
|
|
|
|
{
|
|
|
|
struct device_node *phandle, *remote;
|
|
|
|
struct i2c_adapter *ddc;
|
|
|
|
|
|
|
|
remote = of_graph_get_remote_node(dev->of_node, 1, -1);
|
|
|
|
if (!remote)
|
|
|
|
return ERR_PTR(-EINVAL);
|
|
|
|
|
|
|
|
phandle = of_parse_phandle(remote, "ddc-i2c-bus", 0);
|
|
|
|
of_node_put(remote);
|
|
|
|
if (!phandle)
|
|
|
|
return ERR_PTR(-ENODEV);
|
|
|
|
|
|
|
|
ddc = of_get_i2c_adapter_by_node(phandle);
|
|
|
|
of_node_put(phandle);
|
|
|
|
if (!ddc)
|
|
|
|
return ERR_PTR(-EPROBE_DEFER);
|
|
|
|
|
|
|
|
return ddc;
|
|
|
|
}
|
|
|
|
|
2024-05-27 15:58:18 +02:00
|
|
|
static const struct drm_connector_hdmi_funcs sun4i_hdmi_hdmi_connector_funcs = {
|
|
|
|
.tmds_char_rate_valid = sun4i_hdmi_connector_clock_valid,
|
|
|
|
.write_infoframe = sun4i_hdmi_write_infoframe,
|
|
|
|
};
|
|
|
|
|
2017-05-27 18:09:35 +02:00
|
|
|
static const struct drm_connector_helper_funcs sun4i_hdmi_connector_helper_funcs = {
|
2024-11-30 03:52:35 +02:00
|
|
|
.atomic_check = drm_atomic_helper_connector_hdmi_check,
|
2024-11-30 03:52:30 +02:00
|
|
|
.mode_valid = drm_hdmi_connector_mode_valid,
|
2017-05-27 18:09:35 +02:00
|
|
|
.get_modes = sun4i_hdmi_get_modes,
|
|
|
|
};
|
|
|
|
|
|
|
|
static enum drm_connector_status
|
|
|
|
sun4i_hdmi_connector_detect(struct drm_connector *connector, bool force)
|
|
|
|
{
|
|
|
|
struct sun4i_hdmi *hdmi = drm_connector_to_sun4i_hdmi(connector);
|
|
|
|
unsigned long reg;
|
|
|
|
|
2020-06-29 14:00:32 +08:00
|
|
|
reg = readl(hdmi->base + SUN4I_HDMI_HPD_REG);
|
2020-07-11 09:10:30 +08:00
|
|
|
if (!(reg & SUN4I_HDMI_HPD_HIGH)) {
|
2017-07-11 08:30:44 +02:00
|
|
|
cec_phys_addr_invalidate(hdmi->cec_adap);
|
2017-05-27 18:09:35 +02:00
|
|
|
return connector_status_disconnected;
|
2017-07-11 08:30:44 +02:00
|
|
|
}
|
2017-05-27 18:09:35 +02:00
|
|
|
|
|
|
|
return connector_status_connected;
|
|
|
|
}
|
|
|
|
|
2024-05-27 15:58:18 +02:00
|
|
|
static void sun4i_hdmi_connector_reset(struct drm_connector *connector)
|
|
|
|
{
|
|
|
|
drm_atomic_helper_connector_reset(connector);
|
|
|
|
__drm_atomic_helper_connector_hdmi_reset(connector, connector->state);
|
|
|
|
}
|
|
|
|
|
2017-05-27 18:09:35 +02:00
|
|
|
static const struct drm_connector_funcs sun4i_hdmi_connector_funcs = {
|
|
|
|
.detect = sun4i_hdmi_connector_detect,
|
|
|
|
.fill_modes = drm_helper_probe_single_connector_modes,
|
2024-05-27 15:58:18 +02:00
|
|
|
.reset = sun4i_hdmi_connector_reset,
|
2017-05-27 18:09:35 +02:00
|
|
|
.atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state,
|
|
|
|
.atomic_destroy_state = drm_atomic_helper_connector_destroy_state,
|
|
|
|
};
|
|
|
|
|
2017-07-11 08:30:44 +02:00
|
|
|
#ifdef CONFIG_DRM_SUN4I_HDMI_CEC
|
2020-04-21 11:23:41 +02:00
|
|
|
static int sun4i_hdmi_cec_pin_read(struct cec_adapter *adap)
|
2017-07-11 08:30:44 +02:00
|
|
|
{
|
|
|
|
struct sun4i_hdmi *hdmi = cec_get_drvdata(adap);
|
|
|
|
|
|
|
|
return readl(hdmi->base + SUN4I_HDMI_CEC) & SUN4I_HDMI_CEC_RX;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void sun4i_hdmi_cec_pin_low(struct cec_adapter *adap)
|
|
|
|
{
|
|
|
|
struct sun4i_hdmi *hdmi = cec_get_drvdata(adap);
|
|
|
|
|
|
|
|
/* Start driving the CEC pin low */
|
|
|
|
writel(SUN4I_HDMI_CEC_ENABLE, hdmi->base + SUN4I_HDMI_CEC);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void sun4i_hdmi_cec_pin_high(struct cec_adapter *adap)
|
|
|
|
{
|
|
|
|
struct sun4i_hdmi *hdmi = cec_get_drvdata(adap);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Stop driving the CEC pin, the pull up will take over
|
|
|
|
* unless another CEC device is driving the pin low.
|
|
|
|
*/
|
|
|
|
writel(0, hdmi->base + SUN4I_HDMI_CEC);
|
|
|
|
}
|
|
|
|
|
|
|
|
static const struct cec_pin_ops sun4i_hdmi_cec_pin_ops = {
|
|
|
|
.read = sun4i_hdmi_cec_pin_read,
|
|
|
|
.low = sun4i_hdmi_cec_pin_low,
|
|
|
|
.high = sun4i_hdmi_cec_pin_high,
|
|
|
|
};
|
|
|
|
#endif
|
|
|
|
|
drm/sun4i: hdmi: Add support for controller hardware variants
The HDMI controller found in earlier Allwinner SoCs have slight
differences between the A10, A10s, and the A31:
- Need different initial values for the PLL related registers
- Different behavior of the DDC and TMDS clocks
- Different register layout for the DDC portion
- Separate DDC parent clock on the A31
- Explicit reset control
For the A31, the HDMI TMDS clock has a different value offset for
the divider. The HDMI DDC block is different from the one in the
other SoCs. As far as the DDC clock goes, it has no pre-divider,
as it is clocked from a slower parent clock, not the TMDS clock.
The divider offset from the register value is different. And the
clock control register is at a different offset.
A new variant data structure is created to store pointers to the
above functions, structures, and the different initial values.
Another flag notates whether there is a separate DDC parent clock.
If not, the TMDS clock is passed to the DDC clock create function,
as before.
Regmap fields are used to deal with the different register layout
of the DDC block.
Signed-off-by: Chen-Yu Tsai <wens@csie.org>
Acked-by: Maxime Ripard <maxime.ripard@free-electrons.com>
Signed-off-by: Maxime Ripard <maxime.ripard@free-electrons.com>
Link: https://patchwork.freedesktop.org/patch/msgid/20171010032008.682-8-wens@csie.org
2017-10-10 11:20:04 +08:00
|
|
|
#define SUN4I_HDMI_PAD_CTRL1_MASK (GENMASK(24, 7) | GENMASK(5, 0))
|
|
|
|
#define SUN4I_HDMI_PLL_CTRL_MASK (GENMASK(31, 8) | GENMASK(3, 0))
|
|
|
|
|
2017-10-17 20:18:00 +08:00
|
|
|
/* Only difference from sun5i is AMP is 4 instead of 6 */
|
|
|
|
static const struct sun4i_hdmi_variant sun4i_variant = {
|
|
|
|
.pad_ctrl0_init_val = SUN4I_HDMI_PAD_CTRL0_TXEN |
|
|
|
|
SUN4I_HDMI_PAD_CTRL0_CKEN |
|
|
|
|
SUN4I_HDMI_PAD_CTRL0_PWENG |
|
|
|
|
SUN4I_HDMI_PAD_CTRL0_PWEND |
|
|
|
|
SUN4I_HDMI_PAD_CTRL0_PWENC |
|
|
|
|
SUN4I_HDMI_PAD_CTRL0_LDODEN |
|
|
|
|
SUN4I_HDMI_PAD_CTRL0_LDOCEN |
|
|
|
|
SUN4I_HDMI_PAD_CTRL0_BIASEN,
|
|
|
|
.pad_ctrl1_init_val = SUN4I_HDMI_PAD_CTRL1_REG_AMP(4) |
|
|
|
|
SUN4I_HDMI_PAD_CTRL1_REG_EMP(2) |
|
|
|
|
SUN4I_HDMI_PAD_CTRL1_REG_DENCK |
|
|
|
|
SUN4I_HDMI_PAD_CTRL1_REG_DEN |
|
|
|
|
SUN4I_HDMI_PAD_CTRL1_EMPCK_OPT |
|
|
|
|
SUN4I_HDMI_PAD_CTRL1_EMP_OPT |
|
|
|
|
SUN4I_HDMI_PAD_CTRL1_AMPCK_OPT |
|
|
|
|
SUN4I_HDMI_PAD_CTRL1_AMP_OPT,
|
|
|
|
.pll_ctrl_init_val = SUN4I_HDMI_PLL_CTRL_VCO_S(8) |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_CS(7) |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_CP_S(15) |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_S(7) |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_VCO_GAIN(4) |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_SDIV2 |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_LDO2_EN |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_LDO1_EN |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_HV_IS_33 |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_BWS |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_PLL_EN,
|
|
|
|
|
|
|
|
.ddc_clk_reg = REG_FIELD(SUN4I_HDMI_DDC_CLK_REG, 0, 6),
|
|
|
|
.ddc_clk_pre_divider = 2,
|
|
|
|
.ddc_clk_m_offset = 1,
|
|
|
|
|
|
|
|
.field_ddc_en = REG_FIELD(SUN4I_HDMI_DDC_CTRL_REG, 31, 31),
|
|
|
|
.field_ddc_start = REG_FIELD(SUN4I_HDMI_DDC_CTRL_REG, 30, 30),
|
|
|
|
.field_ddc_reset = REG_FIELD(SUN4I_HDMI_DDC_CTRL_REG, 0, 0),
|
|
|
|
.field_ddc_addr_reg = REG_FIELD(SUN4I_HDMI_DDC_ADDR_REG, 0, 31),
|
|
|
|
.field_ddc_slave_addr = REG_FIELD(SUN4I_HDMI_DDC_ADDR_REG, 0, 6),
|
|
|
|
.field_ddc_int_status = REG_FIELD(SUN4I_HDMI_DDC_INT_STATUS_REG, 0, 8),
|
|
|
|
.field_ddc_fifo_clear = REG_FIELD(SUN4I_HDMI_DDC_FIFO_CTRL_REG, 31, 31),
|
|
|
|
.field_ddc_fifo_rx_thres = REG_FIELD(SUN4I_HDMI_DDC_FIFO_CTRL_REG, 4, 7),
|
|
|
|
.field_ddc_fifo_tx_thres = REG_FIELD(SUN4I_HDMI_DDC_FIFO_CTRL_REG, 0, 3),
|
|
|
|
.field_ddc_byte_count = REG_FIELD(SUN4I_HDMI_DDC_BYTE_COUNT_REG, 0, 9),
|
|
|
|
.field_ddc_cmd = REG_FIELD(SUN4I_HDMI_DDC_CMD_REG, 0, 2),
|
|
|
|
.field_ddc_sda_en = REG_FIELD(SUN4I_HDMI_DDC_LINE_CTRL_REG, 9, 9),
|
|
|
|
.field_ddc_sck_en = REG_FIELD(SUN4I_HDMI_DDC_LINE_CTRL_REG, 8, 8),
|
|
|
|
|
|
|
|
.ddc_fifo_reg = SUN4I_HDMI_DDC_FIFO_DATA_REG,
|
|
|
|
.ddc_fifo_has_dir = true,
|
|
|
|
};
|
|
|
|
|
drm/sun4i: hdmi: Add support for controller hardware variants
The HDMI controller found in earlier Allwinner SoCs have slight
differences between the A10, A10s, and the A31:
- Need different initial values for the PLL related registers
- Different behavior of the DDC and TMDS clocks
- Different register layout for the DDC portion
- Separate DDC parent clock on the A31
- Explicit reset control
For the A31, the HDMI TMDS clock has a different value offset for
the divider. The HDMI DDC block is different from the one in the
other SoCs. As far as the DDC clock goes, it has no pre-divider,
as it is clocked from a slower parent clock, not the TMDS clock.
The divider offset from the register value is different. And the
clock control register is at a different offset.
A new variant data structure is created to store pointers to the
above functions, structures, and the different initial values.
Another flag notates whether there is a separate DDC parent clock.
If not, the TMDS clock is passed to the DDC clock create function,
as before.
Regmap fields are used to deal with the different register layout
of the DDC block.
Signed-off-by: Chen-Yu Tsai <wens@csie.org>
Acked-by: Maxime Ripard <maxime.ripard@free-electrons.com>
Signed-off-by: Maxime Ripard <maxime.ripard@free-electrons.com>
Link: https://patchwork.freedesktop.org/patch/msgid/20171010032008.682-8-wens@csie.org
2017-10-10 11:20:04 +08:00
|
|
|
static const struct sun4i_hdmi_variant sun5i_variant = {
|
|
|
|
.pad_ctrl0_init_val = SUN4I_HDMI_PAD_CTRL0_TXEN |
|
|
|
|
SUN4I_HDMI_PAD_CTRL0_CKEN |
|
|
|
|
SUN4I_HDMI_PAD_CTRL0_PWENG |
|
|
|
|
SUN4I_HDMI_PAD_CTRL0_PWEND |
|
|
|
|
SUN4I_HDMI_PAD_CTRL0_PWENC |
|
|
|
|
SUN4I_HDMI_PAD_CTRL0_LDODEN |
|
|
|
|
SUN4I_HDMI_PAD_CTRL0_LDOCEN |
|
|
|
|
SUN4I_HDMI_PAD_CTRL0_BIASEN,
|
|
|
|
.pad_ctrl1_init_val = SUN4I_HDMI_PAD_CTRL1_REG_AMP(6) |
|
|
|
|
SUN4I_HDMI_PAD_CTRL1_REG_EMP(2) |
|
|
|
|
SUN4I_HDMI_PAD_CTRL1_REG_DENCK |
|
|
|
|
SUN4I_HDMI_PAD_CTRL1_REG_DEN |
|
|
|
|
SUN4I_HDMI_PAD_CTRL1_EMPCK_OPT |
|
|
|
|
SUN4I_HDMI_PAD_CTRL1_EMP_OPT |
|
|
|
|
SUN4I_HDMI_PAD_CTRL1_AMPCK_OPT |
|
|
|
|
SUN4I_HDMI_PAD_CTRL1_AMP_OPT,
|
|
|
|
.pll_ctrl_init_val = SUN4I_HDMI_PLL_CTRL_VCO_S(8) |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_CS(7) |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_CP_S(15) |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_S(7) |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_VCO_GAIN(4) |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_SDIV2 |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_LDO2_EN |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_LDO1_EN |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_HV_IS_33 |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_BWS |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_PLL_EN,
|
|
|
|
|
|
|
|
.ddc_clk_reg = REG_FIELD(SUN4I_HDMI_DDC_CLK_REG, 0, 6),
|
|
|
|
.ddc_clk_pre_divider = 2,
|
|
|
|
.ddc_clk_m_offset = 1,
|
|
|
|
|
|
|
|
.field_ddc_en = REG_FIELD(SUN4I_HDMI_DDC_CTRL_REG, 31, 31),
|
|
|
|
.field_ddc_start = REG_FIELD(SUN4I_HDMI_DDC_CTRL_REG, 30, 30),
|
|
|
|
.field_ddc_reset = REG_FIELD(SUN4I_HDMI_DDC_CTRL_REG, 0, 0),
|
|
|
|
.field_ddc_addr_reg = REG_FIELD(SUN4I_HDMI_DDC_ADDR_REG, 0, 31),
|
|
|
|
.field_ddc_slave_addr = REG_FIELD(SUN4I_HDMI_DDC_ADDR_REG, 0, 6),
|
|
|
|
.field_ddc_int_status = REG_FIELD(SUN4I_HDMI_DDC_INT_STATUS_REG, 0, 8),
|
|
|
|
.field_ddc_fifo_clear = REG_FIELD(SUN4I_HDMI_DDC_FIFO_CTRL_REG, 31, 31),
|
|
|
|
.field_ddc_fifo_rx_thres = REG_FIELD(SUN4I_HDMI_DDC_FIFO_CTRL_REG, 4, 7),
|
|
|
|
.field_ddc_fifo_tx_thres = REG_FIELD(SUN4I_HDMI_DDC_FIFO_CTRL_REG, 0, 3),
|
|
|
|
.field_ddc_byte_count = REG_FIELD(SUN4I_HDMI_DDC_BYTE_COUNT_REG, 0, 9),
|
|
|
|
.field_ddc_cmd = REG_FIELD(SUN4I_HDMI_DDC_CMD_REG, 0, 2),
|
|
|
|
.field_ddc_sda_en = REG_FIELD(SUN4I_HDMI_DDC_LINE_CTRL_REG, 9, 9),
|
|
|
|
.field_ddc_sck_en = REG_FIELD(SUN4I_HDMI_DDC_LINE_CTRL_REG, 8, 8),
|
|
|
|
|
|
|
|
.ddc_fifo_reg = SUN4I_HDMI_DDC_FIFO_DATA_REG,
|
|
|
|
.ddc_fifo_has_dir = true,
|
|
|
|
};
|
|
|
|
|
2017-10-10 11:20:06 +08:00
|
|
|
static const struct sun4i_hdmi_variant sun6i_variant = {
|
|
|
|
.has_ddc_parent_clk = true,
|
|
|
|
.has_reset_control = true,
|
|
|
|
.pad_ctrl0_init_val = 0xff |
|
|
|
|
SUN4I_HDMI_PAD_CTRL0_TXEN |
|
|
|
|
SUN4I_HDMI_PAD_CTRL0_CKEN |
|
|
|
|
SUN4I_HDMI_PAD_CTRL0_PWENG |
|
|
|
|
SUN4I_HDMI_PAD_CTRL0_PWEND |
|
|
|
|
SUN4I_HDMI_PAD_CTRL0_PWENC |
|
|
|
|
SUN4I_HDMI_PAD_CTRL0_LDODEN |
|
|
|
|
SUN4I_HDMI_PAD_CTRL0_LDOCEN,
|
|
|
|
.pad_ctrl1_init_val = SUN4I_HDMI_PAD_CTRL1_REG_AMP(6) |
|
|
|
|
SUN4I_HDMI_PAD_CTRL1_REG_EMP(4) |
|
|
|
|
SUN4I_HDMI_PAD_CTRL1_REG_DENCK |
|
|
|
|
SUN4I_HDMI_PAD_CTRL1_REG_DEN |
|
|
|
|
SUN4I_HDMI_PAD_CTRL1_EMPCK_OPT |
|
|
|
|
SUN4I_HDMI_PAD_CTRL1_EMP_OPT |
|
|
|
|
SUN4I_HDMI_PAD_CTRL1_PWSDT |
|
|
|
|
SUN4I_HDMI_PAD_CTRL1_PWSCK |
|
|
|
|
SUN4I_HDMI_PAD_CTRL1_AMPCK_OPT |
|
|
|
|
SUN4I_HDMI_PAD_CTRL1_AMP_OPT |
|
|
|
|
SUN4I_HDMI_PAD_CTRL1_UNKNOWN,
|
|
|
|
.pll_ctrl_init_val = SUN4I_HDMI_PLL_CTRL_VCO_S(8) |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_CS(3) |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_CP_S(10) |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_S(4) |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_VCO_GAIN(4) |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_SDIV2 |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_LDO2_EN |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_LDO1_EN |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_HV_IS_33 |
|
|
|
|
SUN4I_HDMI_PLL_CTRL_PLL_EN,
|
|
|
|
|
|
|
|
.ddc_clk_reg = REG_FIELD(SUN6I_HDMI_DDC_CLK_REG, 0, 6),
|
|
|
|
.ddc_clk_pre_divider = 1,
|
|
|
|
.ddc_clk_m_offset = 2,
|
|
|
|
|
|
|
|
.tmds_clk_div_offset = 1,
|
|
|
|
|
|
|
|
.field_ddc_en = REG_FIELD(SUN6I_HDMI_DDC_CTRL_REG, 0, 0),
|
|
|
|
.field_ddc_start = REG_FIELD(SUN6I_HDMI_DDC_CTRL_REG, 27, 27),
|
|
|
|
.field_ddc_reset = REG_FIELD(SUN6I_HDMI_DDC_CTRL_REG, 31, 31),
|
|
|
|
.field_ddc_addr_reg = REG_FIELD(SUN6I_HDMI_DDC_ADDR_REG, 1, 31),
|
|
|
|
.field_ddc_slave_addr = REG_FIELD(SUN6I_HDMI_DDC_ADDR_REG, 1, 7),
|
|
|
|
.field_ddc_int_status = REG_FIELD(SUN6I_HDMI_DDC_INT_STATUS_REG, 0, 8),
|
|
|
|
.field_ddc_fifo_clear = REG_FIELD(SUN6I_HDMI_DDC_FIFO_CTRL_REG, 18, 18),
|
|
|
|
.field_ddc_fifo_rx_thres = REG_FIELD(SUN6I_HDMI_DDC_FIFO_CTRL_REG, 4, 7),
|
|
|
|
.field_ddc_fifo_tx_thres = REG_FIELD(SUN6I_HDMI_DDC_FIFO_CTRL_REG, 0, 3),
|
|
|
|
.field_ddc_byte_count = REG_FIELD(SUN6I_HDMI_DDC_CMD_REG, 16, 25),
|
|
|
|
.field_ddc_cmd = REG_FIELD(SUN6I_HDMI_DDC_CMD_REG, 0, 2),
|
|
|
|
.field_ddc_sda_en = REG_FIELD(SUN6I_HDMI_DDC_CTRL_REG, 6, 6),
|
|
|
|
.field_ddc_sck_en = REG_FIELD(SUN6I_HDMI_DDC_CTRL_REG, 4, 4),
|
|
|
|
|
|
|
|
.ddc_fifo_reg = SUN6I_HDMI_DDC_FIFO_DATA_REG,
|
|
|
|
.ddc_fifo_thres_incl = true,
|
|
|
|
};
|
|
|
|
|
2017-10-10 11:20:01 +08:00
|
|
|
static const struct regmap_config sun4i_hdmi_regmap_config = {
|
|
|
|
.reg_bits = 32,
|
|
|
|
.val_bits = 32,
|
|
|
|
.reg_stride = 4,
|
|
|
|
.max_register = 0x580,
|
|
|
|
};
|
|
|
|
|
2017-05-27 18:09:35 +02:00
|
|
|
static int sun4i_hdmi_bind(struct device *dev, struct device *master,
|
|
|
|
void *data)
|
|
|
|
{
|
|
|
|
struct platform_device *pdev = to_platform_device(dev);
|
|
|
|
struct drm_device *drm = data;
|
2019-08-23 13:24:26 +02:00
|
|
|
struct cec_connector_info conn_info;
|
2017-05-27 18:09:35 +02:00
|
|
|
struct sun4i_drv *drv = drm->dev_private;
|
|
|
|
struct sun4i_hdmi *hdmi;
|
|
|
|
u32 reg;
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
hdmi = devm_kzalloc(dev, sizeof(*hdmi), GFP_KERNEL);
|
|
|
|
if (!hdmi)
|
|
|
|
return -ENOMEM;
|
|
|
|
dev_set_drvdata(dev, hdmi);
|
|
|
|
hdmi->dev = dev;
|
|
|
|
hdmi->drv = drv;
|
|
|
|
|
drm/sun4i: hdmi: Add support for controller hardware variants
The HDMI controller found in earlier Allwinner SoCs have slight
differences between the A10, A10s, and the A31:
- Need different initial values for the PLL related registers
- Different behavior of the DDC and TMDS clocks
- Different register layout for the DDC portion
- Separate DDC parent clock on the A31
- Explicit reset control
For the A31, the HDMI TMDS clock has a different value offset for
the divider. The HDMI DDC block is different from the one in the
other SoCs. As far as the DDC clock goes, it has no pre-divider,
as it is clocked from a slower parent clock, not the TMDS clock.
The divider offset from the register value is different. And the
clock control register is at a different offset.
A new variant data structure is created to store pointers to the
above functions, structures, and the different initial values.
Another flag notates whether there is a separate DDC parent clock.
If not, the TMDS clock is passed to the DDC clock create function,
as before.
Regmap fields are used to deal with the different register layout
of the DDC block.
Signed-off-by: Chen-Yu Tsai <wens@csie.org>
Acked-by: Maxime Ripard <maxime.ripard@free-electrons.com>
Signed-off-by: Maxime Ripard <maxime.ripard@free-electrons.com>
Link: https://patchwork.freedesktop.org/patch/msgid/20171010032008.682-8-wens@csie.org
2017-10-10 11:20:04 +08:00
|
|
|
hdmi->variant = of_device_get_match_data(dev);
|
|
|
|
if (!hdmi->variant)
|
|
|
|
return -EINVAL;
|
|
|
|
|
2021-08-31 21:57:39 +08:00
|
|
|
hdmi->base = devm_platform_ioremap_resource(pdev, 0);
|
2017-05-27 18:09:35 +02:00
|
|
|
if (IS_ERR(hdmi->base)) {
|
|
|
|
dev_err(dev, "Couldn't map the HDMI encoder registers\n");
|
|
|
|
return PTR_ERR(hdmi->base);
|
|
|
|
}
|
|
|
|
|
drm/sun4i: hdmi: Add support for controller hardware variants
The HDMI controller found in earlier Allwinner SoCs have slight
differences between the A10, A10s, and the A31:
- Need different initial values for the PLL related registers
- Different behavior of the DDC and TMDS clocks
- Different register layout for the DDC portion
- Separate DDC parent clock on the A31
- Explicit reset control
For the A31, the HDMI TMDS clock has a different value offset for
the divider. The HDMI DDC block is different from the one in the
other SoCs. As far as the DDC clock goes, it has no pre-divider,
as it is clocked from a slower parent clock, not the TMDS clock.
The divider offset from the register value is different. And the
clock control register is at a different offset.
A new variant data structure is created to store pointers to the
above functions, structures, and the different initial values.
Another flag notates whether there is a separate DDC parent clock.
If not, the TMDS clock is passed to the DDC clock create function,
as before.
Regmap fields are used to deal with the different register layout
of the DDC block.
Signed-off-by: Chen-Yu Tsai <wens@csie.org>
Acked-by: Maxime Ripard <maxime.ripard@free-electrons.com>
Signed-off-by: Maxime Ripard <maxime.ripard@free-electrons.com>
Link: https://patchwork.freedesktop.org/patch/msgid/20171010032008.682-8-wens@csie.org
2017-10-10 11:20:04 +08:00
|
|
|
if (hdmi->variant->has_reset_control) {
|
|
|
|
hdmi->reset = devm_reset_control_get(dev, NULL);
|
|
|
|
if (IS_ERR(hdmi->reset)) {
|
|
|
|
dev_err(dev, "Couldn't get the HDMI reset control\n");
|
|
|
|
return PTR_ERR(hdmi->reset);
|
|
|
|
}
|
|
|
|
|
|
|
|
ret = reset_control_deassert(hdmi->reset);
|
|
|
|
if (ret) {
|
|
|
|
dev_err(dev, "Couldn't deassert HDMI reset\n");
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-27 18:09:35 +02:00
|
|
|
hdmi->bus_clk = devm_clk_get(dev, "ahb");
|
|
|
|
if (IS_ERR(hdmi->bus_clk)) {
|
|
|
|
dev_err(dev, "Couldn't get the HDMI bus clock\n");
|
drm/sun4i: hdmi: Add support for controller hardware variants
The HDMI controller found in earlier Allwinner SoCs have slight
differences between the A10, A10s, and the A31:
- Need different initial values for the PLL related registers
- Different behavior of the DDC and TMDS clocks
- Different register layout for the DDC portion
- Separate DDC parent clock on the A31
- Explicit reset control
For the A31, the HDMI TMDS clock has a different value offset for
the divider. The HDMI DDC block is different from the one in the
other SoCs. As far as the DDC clock goes, it has no pre-divider,
as it is clocked from a slower parent clock, not the TMDS clock.
The divider offset from the register value is different. And the
clock control register is at a different offset.
A new variant data structure is created to store pointers to the
above functions, structures, and the different initial values.
Another flag notates whether there is a separate DDC parent clock.
If not, the TMDS clock is passed to the DDC clock create function,
as before.
Regmap fields are used to deal with the different register layout
of the DDC block.
Signed-off-by: Chen-Yu Tsai <wens@csie.org>
Acked-by: Maxime Ripard <maxime.ripard@free-electrons.com>
Signed-off-by: Maxime Ripard <maxime.ripard@free-electrons.com>
Link: https://patchwork.freedesktop.org/patch/msgid/20171010032008.682-8-wens@csie.org
2017-10-10 11:20:04 +08:00
|
|
|
ret = PTR_ERR(hdmi->bus_clk);
|
|
|
|
goto err_assert_reset;
|
2017-05-27 18:09:35 +02:00
|
|
|
}
|
|
|
|
clk_prepare_enable(hdmi->bus_clk);
|
|
|
|
|
|
|
|
hdmi->mod_clk = devm_clk_get(dev, "mod");
|
|
|
|
if (IS_ERR(hdmi->mod_clk)) {
|
|
|
|
dev_err(dev, "Couldn't get the HDMI mod clock\n");
|
2017-10-10 11:20:00 +08:00
|
|
|
ret = PTR_ERR(hdmi->mod_clk);
|
|
|
|
goto err_disable_bus_clk;
|
2017-05-27 18:09:35 +02:00
|
|
|
}
|
|
|
|
clk_prepare_enable(hdmi->mod_clk);
|
|
|
|
|
|
|
|
hdmi->pll0_clk = devm_clk_get(dev, "pll-0");
|
|
|
|
if (IS_ERR(hdmi->pll0_clk)) {
|
|
|
|
dev_err(dev, "Couldn't get the HDMI PLL 0 clock\n");
|
2017-10-10 11:20:00 +08:00
|
|
|
ret = PTR_ERR(hdmi->pll0_clk);
|
|
|
|
goto err_disable_mod_clk;
|
2017-05-27 18:09:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
hdmi->pll1_clk = devm_clk_get(dev, "pll-1");
|
|
|
|
if (IS_ERR(hdmi->pll1_clk)) {
|
|
|
|
dev_err(dev, "Couldn't get the HDMI PLL 1 clock\n");
|
2017-10-10 11:20:00 +08:00
|
|
|
ret = PTR_ERR(hdmi->pll1_clk);
|
|
|
|
goto err_disable_mod_clk;
|
2017-05-27 18:09:35 +02:00
|
|
|
}
|
|
|
|
|
2017-10-10 11:20:01 +08:00
|
|
|
hdmi->regmap = devm_regmap_init_mmio(dev, hdmi->base,
|
|
|
|
&sun4i_hdmi_regmap_config);
|
|
|
|
if (IS_ERR(hdmi->regmap)) {
|
|
|
|
dev_err(dev, "Couldn't create HDMI encoder regmap\n");
|
2018-03-18 23:48:09 +01:00
|
|
|
ret = PTR_ERR(hdmi->regmap);
|
|
|
|
goto err_disable_mod_clk;
|
2017-10-10 11:20:01 +08:00
|
|
|
}
|
|
|
|
|
2017-05-27 18:09:35 +02:00
|
|
|
ret = sun4i_tmds_create(hdmi);
|
|
|
|
if (ret) {
|
|
|
|
dev_err(dev, "Couldn't create the TMDS clock\n");
|
2017-10-10 11:20:00 +08:00
|
|
|
goto err_disable_mod_clk;
|
2017-05-27 18:09:35 +02:00
|
|
|
}
|
|
|
|
|
drm/sun4i: hdmi: Add support for controller hardware variants
The HDMI controller found in earlier Allwinner SoCs have slight
differences between the A10, A10s, and the A31:
- Need different initial values for the PLL related registers
- Different behavior of the DDC and TMDS clocks
- Different register layout for the DDC portion
- Separate DDC parent clock on the A31
- Explicit reset control
For the A31, the HDMI TMDS clock has a different value offset for
the divider. The HDMI DDC block is different from the one in the
other SoCs. As far as the DDC clock goes, it has no pre-divider,
as it is clocked from a slower parent clock, not the TMDS clock.
The divider offset from the register value is different. And the
clock control register is at a different offset.
A new variant data structure is created to store pointers to the
above functions, structures, and the different initial values.
Another flag notates whether there is a separate DDC parent clock.
If not, the TMDS clock is passed to the DDC clock create function,
as before.
Regmap fields are used to deal with the different register layout
of the DDC block.
Signed-off-by: Chen-Yu Tsai <wens@csie.org>
Acked-by: Maxime Ripard <maxime.ripard@free-electrons.com>
Signed-off-by: Maxime Ripard <maxime.ripard@free-electrons.com>
Link: https://patchwork.freedesktop.org/patch/msgid/20171010032008.682-8-wens@csie.org
2017-10-10 11:20:04 +08:00
|
|
|
if (hdmi->variant->has_ddc_parent_clk) {
|
|
|
|
hdmi->ddc_parent_clk = devm_clk_get(dev, "ddc");
|
|
|
|
if (IS_ERR(hdmi->ddc_parent_clk)) {
|
|
|
|
dev_err(dev, "Couldn't get the HDMI DDC clock\n");
|
2018-03-18 23:48:10 +01:00
|
|
|
ret = PTR_ERR(hdmi->ddc_parent_clk);
|
|
|
|
goto err_disable_mod_clk;
|
drm/sun4i: hdmi: Add support for controller hardware variants
The HDMI controller found in earlier Allwinner SoCs have slight
differences between the A10, A10s, and the A31:
- Need different initial values for the PLL related registers
- Different behavior of the DDC and TMDS clocks
- Different register layout for the DDC portion
- Separate DDC parent clock on the A31
- Explicit reset control
For the A31, the HDMI TMDS clock has a different value offset for
the divider. The HDMI DDC block is different from the one in the
other SoCs. As far as the DDC clock goes, it has no pre-divider,
as it is clocked from a slower parent clock, not the TMDS clock.
The divider offset from the register value is different. And the
clock control register is at a different offset.
A new variant data structure is created to store pointers to the
above functions, structures, and the different initial values.
Another flag notates whether there is a separate DDC parent clock.
If not, the TMDS clock is passed to the DDC clock create function,
as before.
Regmap fields are used to deal with the different register layout
of the DDC block.
Signed-off-by: Chen-Yu Tsai <wens@csie.org>
Acked-by: Maxime Ripard <maxime.ripard@free-electrons.com>
Signed-off-by: Maxime Ripard <maxime.ripard@free-electrons.com>
Link: https://patchwork.freedesktop.org/patch/msgid/20171010032008.682-8-wens@csie.org
2017-10-10 11:20:04 +08:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
hdmi->ddc_parent_clk = hdmi->tmds_clk;
|
|
|
|
}
|
|
|
|
|
2017-05-27 18:09:35 +02:00
|
|
|
writel(SUN4I_HDMI_CTRL_ENABLE, hdmi->base + SUN4I_HDMI_CTRL_REG);
|
|
|
|
|
drm/sun4i: hdmi: Add support for controller hardware variants
The HDMI controller found in earlier Allwinner SoCs have slight
differences between the A10, A10s, and the A31:
- Need different initial values for the PLL related registers
- Different behavior of the DDC and TMDS clocks
- Different register layout for the DDC portion
- Separate DDC parent clock on the A31
- Explicit reset control
For the A31, the HDMI TMDS clock has a different value offset for
the divider. The HDMI DDC block is different from the one in the
other SoCs. As far as the DDC clock goes, it has no pre-divider,
as it is clocked from a slower parent clock, not the TMDS clock.
The divider offset from the register value is different. And the
clock control register is at a different offset.
A new variant data structure is created to store pointers to the
above functions, structures, and the different initial values.
Another flag notates whether there is a separate DDC parent clock.
If not, the TMDS clock is passed to the DDC clock create function,
as before.
Regmap fields are used to deal with the different register layout
of the DDC block.
Signed-off-by: Chen-Yu Tsai <wens@csie.org>
Acked-by: Maxime Ripard <maxime.ripard@free-electrons.com>
Signed-off-by: Maxime Ripard <maxime.ripard@free-electrons.com>
Link: https://patchwork.freedesktop.org/patch/msgid/20171010032008.682-8-wens@csie.org
2017-10-10 11:20:04 +08:00
|
|
|
writel(hdmi->variant->pad_ctrl0_init_val,
|
2017-05-27 18:09:35 +02:00
|
|
|
hdmi->base + SUN4I_HDMI_PAD_CTRL0_REG);
|
|
|
|
|
|
|
|
reg = readl(hdmi->base + SUN4I_HDMI_PLL_CTRL_REG);
|
|
|
|
reg &= SUN4I_HDMI_PLL_CTRL_DIV_MASK;
|
drm/sun4i: hdmi: Add support for controller hardware variants
The HDMI controller found in earlier Allwinner SoCs have slight
differences between the A10, A10s, and the A31:
- Need different initial values for the PLL related registers
- Different behavior of the DDC and TMDS clocks
- Different register layout for the DDC portion
- Separate DDC parent clock on the A31
- Explicit reset control
For the A31, the HDMI TMDS clock has a different value offset for
the divider. The HDMI DDC block is different from the one in the
other SoCs. As far as the DDC clock goes, it has no pre-divider,
as it is clocked from a slower parent clock, not the TMDS clock.
The divider offset from the register value is different. And the
clock control register is at a different offset.
A new variant data structure is created to store pointers to the
above functions, structures, and the different initial values.
Another flag notates whether there is a separate DDC parent clock.
If not, the TMDS clock is passed to the DDC clock create function,
as before.
Regmap fields are used to deal with the different register layout
of the DDC block.
Signed-off-by: Chen-Yu Tsai <wens@csie.org>
Acked-by: Maxime Ripard <maxime.ripard@free-electrons.com>
Signed-off-by: Maxime Ripard <maxime.ripard@free-electrons.com>
Link: https://patchwork.freedesktop.org/patch/msgid/20171010032008.682-8-wens@csie.org
2017-10-10 11:20:04 +08:00
|
|
|
reg |= hdmi->variant->pll_ctrl_init_val;
|
2017-05-27 18:09:35 +02:00
|
|
|
writel(reg, hdmi->base + SUN4I_HDMI_PLL_CTRL_REG);
|
|
|
|
|
2017-07-02 17:27:10 +10:00
|
|
|
ret = sun4i_hdmi_i2c_create(dev, hdmi);
|
2017-05-27 18:09:35 +02:00
|
|
|
if (ret) {
|
2017-07-02 17:27:10 +10:00
|
|
|
dev_err(dev, "Couldn't create the HDMI I2C adapter\n");
|
2017-10-10 11:20:00 +08:00
|
|
|
goto err_disable_mod_clk;
|
2017-05-27 18:09:35 +02:00
|
|
|
}
|
|
|
|
|
2019-03-28 13:02:49 +00:00
|
|
|
hdmi->ddc_i2c = sun4i_hdmi_get_ddc(dev);
|
|
|
|
if (IS_ERR(hdmi->ddc_i2c)) {
|
|
|
|
ret = PTR_ERR(hdmi->ddc_i2c);
|
|
|
|
if (ret == -ENODEV)
|
|
|
|
hdmi->ddc_i2c = NULL;
|
|
|
|
else
|
|
|
|
goto err_del_i2c_adapter;
|
|
|
|
}
|
|
|
|
|
2017-05-27 18:09:35 +02:00
|
|
|
drm_encoder_helper_add(&hdmi->encoder,
|
|
|
|
&sun4i_hdmi_helper_funcs);
|
2020-03-05 16:59:42 +01:00
|
|
|
ret = drm_simple_encoder_init(drm, &hdmi->encoder,
|
|
|
|
DRM_MODE_ENCODER_TMDS);
|
2017-05-27 18:09:35 +02:00
|
|
|
if (ret) {
|
|
|
|
dev_err(dev, "Couldn't initialise the HDMI encoder\n");
|
2019-03-28 13:02:49 +00:00
|
|
|
goto err_put_ddc_i2c;
|
2017-05-27 18:09:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
hdmi->encoder.possible_crtcs = drm_of_find_possible_crtcs(drm,
|
|
|
|
dev->of_node);
|
2017-07-02 17:27:10 +10:00
|
|
|
if (!hdmi->encoder.possible_crtcs) {
|
|
|
|
ret = -EPROBE_DEFER;
|
2019-03-28 13:02:49 +00:00
|
|
|
goto err_put_ddc_i2c;
|
2017-07-02 17:27:10 +10:00
|
|
|
}
|
2017-05-27 18:09:35 +02:00
|
|
|
|
2017-07-11 08:30:44 +02:00
|
|
|
#ifdef CONFIG_DRM_SUN4I_HDMI_CEC
|
|
|
|
hdmi->cec_adap = cec_pin_allocate_adapter(&sun4i_hdmi_cec_pin_ops,
|
2019-08-23 13:24:26 +02:00
|
|
|
hdmi, "sun4i", CEC_CAP_DEFAULTS | CEC_CAP_CONNECTOR_INFO);
|
2017-07-11 08:30:44 +02:00
|
|
|
ret = PTR_ERR_OR_ZERO(hdmi->cec_adap);
|
|
|
|
if (ret < 0)
|
|
|
|
goto err_cleanup_connector;
|
|
|
|
writel(readl(hdmi->base + SUN4I_HDMI_CEC) & ~SUN4I_HDMI_CEC_TX,
|
|
|
|
hdmi->base + SUN4I_HDMI_CEC);
|
|
|
|
#endif
|
2017-05-27 18:09:35 +02:00
|
|
|
|
|
|
|
drm_connector_helper_add(&hdmi->connector,
|
|
|
|
&sun4i_hdmi_connector_helper_funcs);
|
2024-05-27 15:58:18 +02:00
|
|
|
ret = drmm_connector_hdmi_init(drm, &hdmi->connector,
|
|
|
|
/*
|
|
|
|
* NOTE: Those are likely to be
|
|
|
|
* wrong, but I couldn't find the
|
|
|
|
* actual ones in the BSP.
|
|
|
|
*/
|
|
|
|
"AW", "HDMI",
|
|
|
|
&sun4i_hdmi_connector_funcs,
|
|
|
|
&sun4i_hdmi_hdmi_connector_funcs,
|
|
|
|
DRM_MODE_CONNECTOR_HDMIA,
|
|
|
|
hdmi->ddc_i2c,
|
|
|
|
BIT(HDMI_COLORSPACE_RGB),
|
|
|
|
8);
|
2017-05-27 18:09:35 +02:00
|
|
|
if (ret) {
|
|
|
|
dev_err(dev,
|
|
|
|
"Couldn't initialise the HDMI connector\n");
|
|
|
|
goto err_cleanup_connector;
|
|
|
|
}
|
2019-08-23 13:24:26 +02:00
|
|
|
cec_fill_conn_info_from_drm(&conn_info, &hdmi->connector);
|
|
|
|
cec_s_conn_info(hdmi->cec_adap, &conn_info);
|
2017-05-27 18:09:35 +02:00
|
|
|
|
|
|
|
/* There is no HPD interrupt, so we need to poll the controller */
|
|
|
|
hdmi->connector.polled = DRM_CONNECTOR_POLL_CONNECT |
|
|
|
|
DRM_CONNECTOR_POLL_DISCONNECT;
|
|
|
|
|
2017-07-11 08:30:44 +02:00
|
|
|
ret = cec_register_adapter(hdmi->cec_adap, dev);
|
|
|
|
if (ret < 0)
|
|
|
|
goto err_cleanup_connector;
|
2018-07-09 10:40:07 +02:00
|
|
|
drm_connector_attach_encoder(&hdmi->connector, &hdmi->encoder);
|
2017-05-27 18:09:35 +02:00
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
err_cleanup_connector:
|
2017-07-11 08:30:44 +02:00
|
|
|
cec_delete_adapter(hdmi->cec_adap);
|
2017-05-27 18:09:35 +02:00
|
|
|
drm_encoder_cleanup(&hdmi->encoder);
|
2019-03-28 13:02:49 +00:00
|
|
|
err_put_ddc_i2c:
|
|
|
|
i2c_put_adapter(hdmi->ddc_i2c);
|
2017-07-02 17:27:10 +10:00
|
|
|
err_del_i2c_adapter:
|
|
|
|
i2c_del_adapter(hdmi->i2c);
|
2017-10-10 11:20:00 +08:00
|
|
|
err_disable_mod_clk:
|
|
|
|
clk_disable_unprepare(hdmi->mod_clk);
|
|
|
|
err_disable_bus_clk:
|
|
|
|
clk_disable_unprepare(hdmi->bus_clk);
|
drm/sun4i: hdmi: Add support for controller hardware variants
The HDMI controller found in earlier Allwinner SoCs have slight
differences between the A10, A10s, and the A31:
- Need different initial values for the PLL related registers
- Different behavior of the DDC and TMDS clocks
- Different register layout for the DDC portion
- Separate DDC parent clock on the A31
- Explicit reset control
For the A31, the HDMI TMDS clock has a different value offset for
the divider. The HDMI DDC block is different from the one in the
other SoCs. As far as the DDC clock goes, it has no pre-divider,
as it is clocked from a slower parent clock, not the TMDS clock.
The divider offset from the register value is different. And the
clock control register is at a different offset.
A new variant data structure is created to store pointers to the
above functions, structures, and the different initial values.
Another flag notates whether there is a separate DDC parent clock.
If not, the TMDS clock is passed to the DDC clock create function,
as before.
Regmap fields are used to deal with the different register layout
of the DDC block.
Signed-off-by: Chen-Yu Tsai <wens@csie.org>
Acked-by: Maxime Ripard <maxime.ripard@free-electrons.com>
Signed-off-by: Maxime Ripard <maxime.ripard@free-electrons.com>
Link: https://patchwork.freedesktop.org/patch/msgid/20171010032008.682-8-wens@csie.org
2017-10-10 11:20:04 +08:00
|
|
|
err_assert_reset:
|
|
|
|
reset_control_assert(hdmi->reset);
|
2017-05-27 18:09:35 +02:00
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void sun4i_hdmi_unbind(struct device *dev, struct device *master,
|
|
|
|
void *data)
|
|
|
|
{
|
|
|
|
struct sun4i_hdmi *hdmi = dev_get_drvdata(dev);
|
|
|
|
|
2017-07-11 08:30:44 +02:00
|
|
|
cec_unregister_adapter(hdmi->cec_adap);
|
2017-07-02 17:27:10 +10:00
|
|
|
i2c_del_adapter(hdmi->i2c);
|
2019-03-28 13:02:49 +00:00
|
|
|
i2c_put_adapter(hdmi->ddc_i2c);
|
2017-10-10 11:20:00 +08:00
|
|
|
clk_disable_unprepare(hdmi->mod_clk);
|
|
|
|
clk_disable_unprepare(hdmi->bus_clk);
|
2017-05-27 18:09:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static const struct component_ops sun4i_hdmi_ops = {
|
|
|
|
.bind = sun4i_hdmi_bind,
|
|
|
|
.unbind = sun4i_hdmi_unbind,
|
|
|
|
};
|
|
|
|
|
|
|
|
static int sun4i_hdmi_probe(struct platform_device *pdev)
|
|
|
|
{
|
|
|
|
return component_add(&pdev->dev, &sun4i_hdmi_ops);
|
|
|
|
}
|
|
|
|
|
2023-05-07 18:26:06 +02:00
|
|
|
static void sun4i_hdmi_remove(struct platform_device *pdev)
|
2017-05-27 18:09:35 +02:00
|
|
|
{
|
|
|
|
component_del(&pdev->dev, &sun4i_hdmi_ops);
|
|
|
|
}
|
|
|
|
|
|
|
|
static const struct of_device_id sun4i_hdmi_of_table[] = {
|
2017-10-17 20:18:00 +08:00
|
|
|
{ .compatible = "allwinner,sun4i-a10-hdmi", .data = &sun4i_variant, },
|
drm/sun4i: hdmi: Add support for controller hardware variants
The HDMI controller found in earlier Allwinner SoCs have slight
differences between the A10, A10s, and the A31:
- Need different initial values for the PLL related registers
- Different behavior of the DDC and TMDS clocks
- Different register layout for the DDC portion
- Separate DDC parent clock on the A31
- Explicit reset control
For the A31, the HDMI TMDS clock has a different value offset for
the divider. The HDMI DDC block is different from the one in the
other SoCs. As far as the DDC clock goes, it has no pre-divider,
as it is clocked from a slower parent clock, not the TMDS clock.
The divider offset from the register value is different. And the
clock control register is at a different offset.
A new variant data structure is created to store pointers to the
above functions, structures, and the different initial values.
Another flag notates whether there is a separate DDC parent clock.
If not, the TMDS clock is passed to the DDC clock create function,
as before.
Regmap fields are used to deal with the different register layout
of the DDC block.
Signed-off-by: Chen-Yu Tsai <wens@csie.org>
Acked-by: Maxime Ripard <maxime.ripard@free-electrons.com>
Signed-off-by: Maxime Ripard <maxime.ripard@free-electrons.com>
Link: https://patchwork.freedesktop.org/patch/msgid/20171010032008.682-8-wens@csie.org
2017-10-10 11:20:04 +08:00
|
|
|
{ .compatible = "allwinner,sun5i-a10s-hdmi", .data = &sun5i_variant, },
|
2017-10-10 11:20:06 +08:00
|
|
|
{ .compatible = "allwinner,sun6i-a31-hdmi", .data = &sun6i_variant, },
|
2017-05-27 18:09:35 +02:00
|
|
|
{ }
|
|
|
|
};
|
|
|
|
MODULE_DEVICE_TABLE(of, sun4i_hdmi_of_table);
|
|
|
|
|
|
|
|
static struct platform_driver sun4i_hdmi_driver = {
|
|
|
|
.probe = sun4i_hdmi_probe,
|
2024-12-01 15:12:43 -08:00
|
|
|
.remove = sun4i_hdmi_remove,
|
2017-05-27 18:09:35 +02:00
|
|
|
.driver = {
|
|
|
|
.name = "sun4i-hdmi",
|
|
|
|
.of_match_table = sun4i_hdmi_of_table,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
module_platform_driver(sun4i_hdmi_driver);
|
|
|
|
|
|
|
|
MODULE_AUTHOR("Maxime Ripard <maxime.ripard@free-electrons.com>");
|
|
|
|
MODULE_DESCRIPTION("Allwinner A10 HDMI Driver");
|
|
|
|
MODULE_LICENSE("GPL");
|