648 lines
16 KiB
C
648 lines
16 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
/*
|
|
* Copyright (c) 2021-2022 Rockchip Electronics Co., Ltd.
|
|
* Copyright (c) 2024 Collabora Ltd.
|
|
*
|
|
* Author: Algea Cao <algea.cao@rock-chips.com>
|
|
* Author: Cristian Ciocaltea <cristian.ciocaltea@collabora.com>
|
|
*/
|
|
#include <linux/completion.h>
|
|
#include <linux/hdmi.h>
|
|
#include <linux/i2c.h>
|
|
#include <linux/irq.h>
|
|
#include <linux/module.h>
|
|
#include <linux/mutex.h>
|
|
#include <linux/of.h>
|
|
#include <linux/workqueue.h>
|
|
|
|
#include <drm/bridge/dw_hdmi_qp.h>
|
|
#include <drm/display/drm_hdmi_helper.h>
|
|
#include <drm/display/drm_hdmi_state_helper.h>
|
|
#include <drm/drm_atomic.h>
|
|
#include <drm/drm_atomic_helper.h>
|
|
#include <drm/drm_bridge.h>
|
|
#include <drm/drm_connector.h>
|
|
#include <drm/drm_edid.h>
|
|
#include <drm/drm_modes.h>
|
|
|
|
#include <sound/hdmi-codec.h>
|
|
|
|
#include "dw-hdmi-qp.h"
|
|
|
|
#define DDC_CI_ADDR 0x37
|
|
#define DDC_SEGMENT_ADDR 0x30
|
|
|
|
#define HDMI14_MAX_TMDSCLK 340000000
|
|
|
|
#define SCRAMB_POLL_DELAY_MS 3000
|
|
|
|
struct dw_hdmi_qp_i2c {
|
|
struct i2c_adapter adap;
|
|
|
|
struct mutex lock; /* used to serialize data transfers */
|
|
struct completion cmp;
|
|
u8 stat;
|
|
|
|
u8 slave_reg;
|
|
bool is_regaddr;
|
|
bool is_segment;
|
|
};
|
|
|
|
struct dw_hdmi_qp {
|
|
struct drm_bridge bridge;
|
|
|
|
struct device *dev;
|
|
struct dw_hdmi_qp_i2c *i2c;
|
|
|
|
struct {
|
|
const struct dw_hdmi_qp_phy_ops *ops;
|
|
void *data;
|
|
} phy;
|
|
|
|
struct regmap *regm;
|
|
};
|
|
|
|
static void dw_hdmi_qp_write(struct dw_hdmi_qp *hdmi, unsigned int val,
|
|
int offset)
|
|
{
|
|
regmap_write(hdmi->regm, offset, val);
|
|
}
|
|
|
|
static unsigned int dw_hdmi_qp_read(struct dw_hdmi_qp *hdmi, int offset)
|
|
{
|
|
unsigned int val = 0;
|
|
|
|
regmap_read(hdmi->regm, offset, &val);
|
|
|
|
return val;
|
|
}
|
|
|
|
static void dw_hdmi_qp_mod(struct dw_hdmi_qp *hdmi, unsigned int data,
|
|
unsigned int mask, unsigned int reg)
|
|
{
|
|
regmap_update_bits(hdmi->regm, reg, mask, data);
|
|
}
|
|
|
|
static int dw_hdmi_qp_i2c_read(struct dw_hdmi_qp *hdmi,
|
|
unsigned char *buf, unsigned int length)
|
|
{
|
|
struct dw_hdmi_qp_i2c *i2c = hdmi->i2c;
|
|
int stat;
|
|
|
|
if (!i2c->is_regaddr) {
|
|
dev_dbg(hdmi->dev, "set read register address to 0\n");
|
|
i2c->slave_reg = 0x00;
|
|
i2c->is_regaddr = true;
|
|
}
|
|
|
|
while (length--) {
|
|
reinit_completion(&i2c->cmp);
|
|
|
|
dw_hdmi_qp_mod(hdmi, i2c->slave_reg++ << 12, I2CM_ADDR,
|
|
I2CM_INTERFACE_CONTROL0);
|
|
|
|
if (i2c->is_segment)
|
|
dw_hdmi_qp_mod(hdmi, I2CM_EXT_READ, I2CM_WR_MASK,
|
|
I2CM_INTERFACE_CONTROL0);
|
|
else
|
|
dw_hdmi_qp_mod(hdmi, I2CM_FM_READ, I2CM_WR_MASK,
|
|
I2CM_INTERFACE_CONTROL0);
|
|
|
|
stat = wait_for_completion_timeout(&i2c->cmp, HZ / 10);
|
|
if (!stat) {
|
|
dev_err(hdmi->dev, "i2c read timed out\n");
|
|
dw_hdmi_qp_write(hdmi, 0x01, I2CM_CONTROL0);
|
|
return -EAGAIN;
|
|
}
|
|
|
|
/* Check for error condition on the bus */
|
|
if (i2c->stat & I2CM_NACK_RCVD_IRQ) {
|
|
dev_err(hdmi->dev, "i2c read error\n");
|
|
dw_hdmi_qp_write(hdmi, 0x01, I2CM_CONTROL0);
|
|
return -EIO;
|
|
}
|
|
|
|
*buf++ = dw_hdmi_qp_read(hdmi, I2CM_INTERFACE_RDDATA_0_3) & 0xff;
|
|
dw_hdmi_qp_mod(hdmi, 0, I2CM_WR_MASK, I2CM_INTERFACE_CONTROL0);
|
|
}
|
|
|
|
i2c->is_segment = false;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dw_hdmi_qp_i2c_write(struct dw_hdmi_qp *hdmi,
|
|
unsigned char *buf, unsigned int length)
|
|
{
|
|
struct dw_hdmi_qp_i2c *i2c = hdmi->i2c;
|
|
int stat;
|
|
|
|
if (!i2c->is_regaddr) {
|
|
/* Use the first write byte as register address */
|
|
i2c->slave_reg = buf[0];
|
|
length--;
|
|
buf++;
|
|
i2c->is_regaddr = true;
|
|
}
|
|
|
|
while (length--) {
|
|
reinit_completion(&i2c->cmp);
|
|
|
|
dw_hdmi_qp_write(hdmi, *buf++, I2CM_INTERFACE_WRDATA_0_3);
|
|
dw_hdmi_qp_mod(hdmi, i2c->slave_reg++ << 12, I2CM_ADDR,
|
|
I2CM_INTERFACE_CONTROL0);
|
|
dw_hdmi_qp_mod(hdmi, I2CM_FM_WRITE, I2CM_WR_MASK,
|
|
I2CM_INTERFACE_CONTROL0);
|
|
|
|
stat = wait_for_completion_timeout(&i2c->cmp, HZ / 10);
|
|
if (!stat) {
|
|
dev_err(hdmi->dev, "i2c write time out!\n");
|
|
dw_hdmi_qp_write(hdmi, 0x01, I2CM_CONTROL0);
|
|
return -EAGAIN;
|
|
}
|
|
|
|
/* Check for error condition on the bus */
|
|
if (i2c->stat & I2CM_NACK_RCVD_IRQ) {
|
|
dev_err(hdmi->dev, "i2c write nack!\n");
|
|
dw_hdmi_qp_write(hdmi, 0x01, I2CM_CONTROL0);
|
|
return -EIO;
|
|
}
|
|
|
|
dw_hdmi_qp_mod(hdmi, 0, I2CM_WR_MASK, I2CM_INTERFACE_CONTROL0);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dw_hdmi_qp_i2c_xfer(struct i2c_adapter *adap,
|
|
struct i2c_msg *msgs, int num)
|
|
{
|
|
struct dw_hdmi_qp *hdmi = i2c_get_adapdata(adap);
|
|
struct dw_hdmi_qp_i2c *i2c = hdmi->i2c;
|
|
u8 addr = msgs[0].addr;
|
|
int i, ret = 0;
|
|
|
|
if (addr == DDC_CI_ADDR)
|
|
/*
|
|
* The internal I2C controller does not support the multi-byte
|
|
* read and write operations needed for DDC/CI.
|
|
* FIXME: Blacklist the DDC/CI address until we filter out
|
|
* unsupported I2C operations.
|
|
*/
|
|
return -EOPNOTSUPP;
|
|
|
|
for (i = 0; i < num; i++) {
|
|
if (msgs[i].len == 0) {
|
|
dev_err(hdmi->dev,
|
|
"unsupported transfer %d/%d, no data\n",
|
|
i + 1, num);
|
|
return -EOPNOTSUPP;
|
|
}
|
|
}
|
|
|
|
guard(mutex)(&i2c->lock);
|
|
|
|
/* Unmute DONE and ERROR interrupts */
|
|
dw_hdmi_qp_mod(hdmi, I2CM_NACK_RCVD_MASK_N | I2CM_OP_DONE_MASK_N,
|
|
I2CM_NACK_RCVD_MASK_N | I2CM_OP_DONE_MASK_N,
|
|
MAINUNIT_1_INT_MASK_N);
|
|
|
|
/* Set slave device address taken from the first I2C message */
|
|
if (addr == DDC_SEGMENT_ADDR && msgs[0].len == 1)
|
|
addr = DDC_ADDR;
|
|
|
|
dw_hdmi_qp_mod(hdmi, addr << 5, I2CM_SLVADDR, I2CM_INTERFACE_CONTROL0);
|
|
|
|
/* Set slave device register address on transfer */
|
|
i2c->is_regaddr = false;
|
|
|
|
/* Set segment pointer for I2C extended read mode operation */
|
|
i2c->is_segment = false;
|
|
|
|
for (i = 0; i < num; i++) {
|
|
if (msgs[i].addr == DDC_SEGMENT_ADDR && msgs[i].len == 1) {
|
|
i2c->is_segment = true;
|
|
dw_hdmi_qp_mod(hdmi, DDC_SEGMENT_ADDR, I2CM_SEG_ADDR,
|
|
I2CM_INTERFACE_CONTROL1);
|
|
dw_hdmi_qp_mod(hdmi, *msgs[i].buf << 7, I2CM_SEG_PTR,
|
|
I2CM_INTERFACE_CONTROL1);
|
|
} else {
|
|
if (msgs[i].flags & I2C_M_RD)
|
|
ret = dw_hdmi_qp_i2c_read(hdmi, msgs[i].buf,
|
|
msgs[i].len);
|
|
else
|
|
ret = dw_hdmi_qp_i2c_write(hdmi, msgs[i].buf,
|
|
msgs[i].len);
|
|
}
|
|
if (ret < 0)
|
|
break;
|
|
}
|
|
|
|
if (!ret)
|
|
ret = num;
|
|
|
|
/* Mute DONE and ERROR interrupts */
|
|
dw_hdmi_qp_mod(hdmi, 0, I2CM_OP_DONE_MASK_N | I2CM_NACK_RCVD_MASK_N,
|
|
MAINUNIT_1_INT_MASK_N);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static u32 dw_hdmi_qp_i2c_func(struct i2c_adapter *adapter)
|
|
{
|
|
return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL;
|
|
}
|
|
|
|
static const struct i2c_algorithm dw_hdmi_qp_algorithm = {
|
|
.master_xfer = dw_hdmi_qp_i2c_xfer,
|
|
.functionality = dw_hdmi_qp_i2c_func,
|
|
};
|
|
|
|
static struct i2c_adapter *dw_hdmi_qp_i2c_adapter(struct dw_hdmi_qp *hdmi)
|
|
{
|
|
struct dw_hdmi_qp_i2c *i2c;
|
|
struct i2c_adapter *adap;
|
|
int ret;
|
|
|
|
i2c = devm_kzalloc(hdmi->dev, sizeof(*i2c), GFP_KERNEL);
|
|
if (!i2c)
|
|
return ERR_PTR(-ENOMEM);
|
|
|
|
mutex_init(&i2c->lock);
|
|
init_completion(&i2c->cmp);
|
|
|
|
adap = &i2c->adap;
|
|
adap->owner = THIS_MODULE;
|
|
adap->dev.parent = hdmi->dev;
|
|
adap->algo = &dw_hdmi_qp_algorithm;
|
|
strscpy(adap->name, "DesignWare HDMI QP", sizeof(adap->name));
|
|
|
|
i2c_set_adapdata(adap, hdmi);
|
|
|
|
ret = devm_i2c_add_adapter(hdmi->dev, adap);
|
|
if (ret) {
|
|
dev_warn(hdmi->dev, "cannot add %s I2C adapter\n", adap->name);
|
|
devm_kfree(hdmi->dev, i2c);
|
|
return ERR_PTR(ret);
|
|
}
|
|
|
|
hdmi->i2c = i2c;
|
|
dev_info(hdmi->dev, "registered %s I2C bus driver\n", adap->name);
|
|
|
|
return adap;
|
|
}
|
|
|
|
static int dw_hdmi_qp_config_avi_infoframe(struct dw_hdmi_qp *hdmi,
|
|
const u8 *buffer, size_t len)
|
|
{
|
|
u32 val, i, j;
|
|
|
|
if (len != HDMI_INFOFRAME_SIZE(AVI)) {
|
|
dev_err(hdmi->dev, "failed to configure avi infoframe\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
/*
|
|
* DW HDMI QP IP uses a different byte format from standard AVI info
|
|
* frames, though generally the bits are in the correct bytes.
|
|
*/
|
|
val = buffer[1] << 8 | buffer[2] << 16;
|
|
dw_hdmi_qp_write(hdmi, val, PKT_AVI_CONTENTS0);
|
|
|
|
for (i = 0; i < 4; i++) {
|
|
for (j = 0; j < 4; j++) {
|
|
if (i * 4 + j >= 14)
|
|
break;
|
|
if (!j)
|
|
val = buffer[i * 4 + j + 3];
|
|
val |= buffer[i * 4 + j + 3] << (8 * j);
|
|
}
|
|
|
|
dw_hdmi_qp_write(hdmi, val, PKT_AVI_CONTENTS1 + i * 4);
|
|
}
|
|
|
|
dw_hdmi_qp_mod(hdmi, 0, PKTSCHED_AVI_FIELDRATE, PKTSCHED_PKT_CONFIG1);
|
|
|
|
dw_hdmi_qp_mod(hdmi, PKTSCHED_AVI_TX_EN | PKTSCHED_GCP_TX_EN,
|
|
PKTSCHED_AVI_TX_EN | PKTSCHED_GCP_TX_EN, PKTSCHED_PKT_EN);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dw_hdmi_qp_config_drm_infoframe(struct dw_hdmi_qp *hdmi,
|
|
const u8 *buffer, size_t len)
|
|
{
|
|
u32 val, i;
|
|
|
|
if (len != HDMI_INFOFRAME_SIZE(DRM)) {
|
|
dev_err(hdmi->dev, "failed to configure drm infoframe\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
dw_hdmi_qp_mod(hdmi, 0, PKTSCHED_DRMI_TX_EN, PKTSCHED_PKT_EN);
|
|
|
|
val = buffer[1] << 8 | buffer[2] << 16;
|
|
dw_hdmi_qp_write(hdmi, val, PKT_DRMI_CONTENTS0);
|
|
|
|
for (i = 0; i <= buffer[2]; i++) {
|
|
if (i % 4 == 0)
|
|
val = buffer[3 + i];
|
|
val |= buffer[3 + i] << ((i % 4) * 8);
|
|
|
|
if ((i % 4 == 3) || i == buffer[2])
|
|
dw_hdmi_qp_write(hdmi, val,
|
|
PKT_DRMI_CONTENTS1 + ((i / 4) * 4));
|
|
}
|
|
|
|
dw_hdmi_qp_mod(hdmi, 0, PKTSCHED_DRMI_FIELDRATE, PKTSCHED_PKT_CONFIG1);
|
|
dw_hdmi_qp_mod(hdmi, PKTSCHED_DRMI_TX_EN, PKTSCHED_DRMI_TX_EN,
|
|
PKTSCHED_PKT_EN);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dw_hdmi_qp_bridge_atomic_check(struct drm_bridge *bridge,
|
|
struct drm_bridge_state *bridge_state,
|
|
struct drm_crtc_state *crtc_state,
|
|
struct drm_connector_state *conn_state)
|
|
{
|
|
struct dw_hdmi_qp *hdmi = bridge->driver_private;
|
|
int ret;
|
|
|
|
ret = drm_atomic_helper_connector_hdmi_check(conn_state->connector,
|
|
conn_state->state);
|
|
if (ret)
|
|
dev_dbg(hdmi->dev, "%s failed: %d\n", __func__, ret);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void dw_hdmi_qp_bridge_atomic_enable(struct drm_bridge *bridge,
|
|
struct drm_bridge_state *old_state)
|
|
{
|
|
struct dw_hdmi_qp *hdmi = bridge->driver_private;
|
|
struct drm_atomic_state *state = old_state->base.state;
|
|
struct drm_connector_state *conn_state;
|
|
struct drm_connector *connector;
|
|
unsigned int op_mode;
|
|
|
|
connector = drm_atomic_get_new_connector_for_encoder(state, bridge->encoder);
|
|
if (WARN_ON(!connector))
|
|
return;
|
|
|
|
conn_state = drm_atomic_get_new_connector_state(state, connector);
|
|
if (WARN_ON(!conn_state))
|
|
return;
|
|
|
|
if (connector->display_info.is_hdmi) {
|
|
dev_dbg(hdmi->dev, "%s mode=HDMI rate=%llu\n",
|
|
__func__, conn_state->hdmi.tmds_char_rate);
|
|
op_mode = 0;
|
|
} else {
|
|
dev_dbg(hdmi->dev, "%s mode=DVI\n", __func__);
|
|
op_mode = OPMODE_DVI;
|
|
}
|
|
|
|
hdmi->phy.ops->init(hdmi, hdmi->phy.data);
|
|
|
|
dw_hdmi_qp_mod(hdmi, HDCP2_BYPASS, HDCP2_BYPASS, HDCP2LOGIC_CONFIG0);
|
|
dw_hdmi_qp_mod(hdmi, op_mode, OPMODE_DVI, LINK_CONFIG0);
|
|
|
|
drm_atomic_helper_connector_hdmi_update_infoframes(connector, state);
|
|
}
|
|
|
|
static void dw_hdmi_qp_bridge_atomic_disable(struct drm_bridge *bridge,
|
|
struct drm_bridge_state *old_state)
|
|
{
|
|
struct dw_hdmi_qp *hdmi = bridge->driver_private;
|
|
|
|
hdmi->phy.ops->disable(hdmi, hdmi->phy.data);
|
|
}
|
|
|
|
static enum drm_connector_status
|
|
dw_hdmi_qp_bridge_detect(struct drm_bridge *bridge)
|
|
{
|
|
struct dw_hdmi_qp *hdmi = bridge->driver_private;
|
|
|
|
return hdmi->phy.ops->read_hpd(hdmi, hdmi->phy.data);
|
|
}
|
|
|
|
static const struct drm_edid *
|
|
dw_hdmi_qp_bridge_edid_read(struct drm_bridge *bridge,
|
|
struct drm_connector *connector)
|
|
{
|
|
struct dw_hdmi_qp *hdmi = bridge->driver_private;
|
|
const struct drm_edid *drm_edid;
|
|
|
|
drm_edid = drm_edid_read_ddc(connector, bridge->ddc);
|
|
if (!drm_edid)
|
|
dev_dbg(hdmi->dev, "failed to get edid\n");
|
|
|
|
return drm_edid;
|
|
}
|
|
|
|
static enum drm_mode_status
|
|
dw_hdmi_qp_bridge_mode_valid(struct drm_bridge *bridge,
|
|
const struct drm_display_info *info,
|
|
const struct drm_display_mode *mode)
|
|
{
|
|
struct dw_hdmi_qp *hdmi = bridge->driver_private;
|
|
unsigned long long rate;
|
|
|
|
rate = drm_hdmi_compute_mode_clock(mode, 8, HDMI_COLORSPACE_RGB);
|
|
if (rate > HDMI14_MAX_TMDSCLK) {
|
|
dev_dbg(hdmi->dev, "Unsupported mode clock: %d\n", mode->clock);
|
|
return MODE_CLOCK_HIGH;
|
|
}
|
|
|
|
return MODE_OK;
|
|
}
|
|
|
|
static int dw_hdmi_qp_bridge_clear_infoframe(struct drm_bridge *bridge,
|
|
enum hdmi_infoframe_type type)
|
|
{
|
|
struct dw_hdmi_qp *hdmi = bridge->driver_private;
|
|
|
|
switch (type) {
|
|
case HDMI_INFOFRAME_TYPE_AVI:
|
|
dw_hdmi_qp_mod(hdmi, 0, PKTSCHED_AVI_TX_EN | PKTSCHED_GCP_TX_EN,
|
|
PKTSCHED_PKT_EN);
|
|
break;
|
|
|
|
case HDMI_INFOFRAME_TYPE_DRM:
|
|
dw_hdmi_qp_mod(hdmi, 0, PKTSCHED_DRMI_TX_EN, PKTSCHED_PKT_EN);
|
|
break;
|
|
|
|
default:
|
|
dev_dbg(hdmi->dev, "Unsupported infoframe type %x\n", type);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dw_hdmi_qp_bridge_write_infoframe(struct drm_bridge *bridge,
|
|
enum hdmi_infoframe_type type,
|
|
const u8 *buffer, size_t len)
|
|
{
|
|
struct dw_hdmi_qp *hdmi = bridge->driver_private;
|
|
|
|
dw_hdmi_qp_bridge_clear_infoframe(bridge, type);
|
|
|
|
switch (type) {
|
|
case HDMI_INFOFRAME_TYPE_AVI:
|
|
return dw_hdmi_qp_config_avi_infoframe(hdmi, buffer, len);
|
|
|
|
case HDMI_INFOFRAME_TYPE_DRM:
|
|
return dw_hdmi_qp_config_drm_infoframe(hdmi, buffer, len);
|
|
|
|
default:
|
|
dev_dbg(hdmi->dev, "Unsupported infoframe type %x\n", type);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static const struct drm_bridge_funcs dw_hdmi_qp_bridge_funcs = {
|
|
.atomic_duplicate_state = drm_atomic_helper_bridge_duplicate_state,
|
|
.atomic_destroy_state = drm_atomic_helper_bridge_destroy_state,
|
|
.atomic_reset = drm_atomic_helper_bridge_reset,
|
|
.atomic_check = dw_hdmi_qp_bridge_atomic_check,
|
|
.atomic_enable = dw_hdmi_qp_bridge_atomic_enable,
|
|
.atomic_disable = dw_hdmi_qp_bridge_atomic_disable,
|
|
.detect = dw_hdmi_qp_bridge_detect,
|
|
.edid_read = dw_hdmi_qp_bridge_edid_read,
|
|
.mode_valid = dw_hdmi_qp_bridge_mode_valid,
|
|
.hdmi_clear_infoframe = dw_hdmi_qp_bridge_clear_infoframe,
|
|
.hdmi_write_infoframe = dw_hdmi_qp_bridge_write_infoframe,
|
|
};
|
|
|
|
static irqreturn_t dw_hdmi_qp_main_hardirq(int irq, void *dev_id)
|
|
{
|
|
struct dw_hdmi_qp *hdmi = dev_id;
|
|
struct dw_hdmi_qp_i2c *i2c = hdmi->i2c;
|
|
u32 stat;
|
|
|
|
stat = dw_hdmi_qp_read(hdmi, MAINUNIT_1_INT_STATUS);
|
|
|
|
i2c->stat = stat & (I2CM_OP_DONE_IRQ | I2CM_READ_REQUEST_IRQ |
|
|
I2CM_NACK_RCVD_IRQ);
|
|
|
|
if (i2c->stat) {
|
|
dw_hdmi_qp_write(hdmi, i2c->stat, MAINUNIT_1_INT_CLEAR);
|
|
complete(&i2c->cmp);
|
|
}
|
|
|
|
if (stat)
|
|
return IRQ_HANDLED;
|
|
|
|
return IRQ_NONE;
|
|
}
|
|
|
|
static const struct regmap_config dw_hdmi_qp_regmap_config = {
|
|
.reg_bits = 32,
|
|
.val_bits = 32,
|
|
.reg_stride = 4,
|
|
.max_register = EARCRX_1_INT_FORCE,
|
|
};
|
|
|
|
static void dw_hdmi_qp_init_hw(struct dw_hdmi_qp *hdmi)
|
|
{
|
|
dw_hdmi_qp_write(hdmi, 0, MAINUNIT_0_INT_MASK_N);
|
|
dw_hdmi_qp_write(hdmi, 0, MAINUNIT_1_INT_MASK_N);
|
|
dw_hdmi_qp_write(hdmi, 428571429, TIMER_BASE_CONFIG0);
|
|
|
|
/* Software reset */
|
|
dw_hdmi_qp_write(hdmi, 0x01, I2CM_CONTROL0);
|
|
|
|
dw_hdmi_qp_write(hdmi, 0x085c085c, I2CM_FM_SCL_CONFIG0);
|
|
|
|
dw_hdmi_qp_mod(hdmi, 0, I2CM_FM_EN, I2CM_INTERFACE_CONTROL0);
|
|
|
|
/* Clear DONE and ERROR interrupts */
|
|
dw_hdmi_qp_write(hdmi, I2CM_OP_DONE_CLEAR | I2CM_NACK_RCVD_CLEAR,
|
|
MAINUNIT_1_INT_CLEAR);
|
|
|
|
if (hdmi->phy.ops->setup_hpd)
|
|
hdmi->phy.ops->setup_hpd(hdmi, hdmi->phy.data);
|
|
}
|
|
|
|
struct dw_hdmi_qp *dw_hdmi_qp_bind(struct platform_device *pdev,
|
|
struct drm_encoder *encoder,
|
|
const struct dw_hdmi_qp_plat_data *plat_data)
|
|
{
|
|
struct device *dev = &pdev->dev;
|
|
struct dw_hdmi_qp *hdmi;
|
|
void __iomem *regs;
|
|
int ret;
|
|
|
|
if (!plat_data->phy_ops || !plat_data->phy_ops->init ||
|
|
!plat_data->phy_ops->disable || !plat_data->phy_ops->read_hpd) {
|
|
dev_err(dev, "Missing platform PHY ops\n");
|
|
return ERR_PTR(-ENODEV);
|
|
}
|
|
|
|
hdmi = devm_kzalloc(dev, sizeof(*hdmi), GFP_KERNEL);
|
|
if (!hdmi)
|
|
return ERR_PTR(-ENOMEM);
|
|
|
|
hdmi->dev = dev;
|
|
|
|
regs = devm_platform_ioremap_resource(pdev, 0);
|
|
if (IS_ERR(regs))
|
|
return ERR_CAST(regs);
|
|
|
|
hdmi->regm = devm_regmap_init_mmio(dev, regs, &dw_hdmi_qp_regmap_config);
|
|
if (IS_ERR(hdmi->regm)) {
|
|
dev_err(dev, "Failed to configure regmap\n");
|
|
return ERR_CAST(hdmi->regm);
|
|
}
|
|
|
|
hdmi->phy.ops = plat_data->phy_ops;
|
|
hdmi->phy.data = plat_data->phy_data;
|
|
|
|
dw_hdmi_qp_init_hw(hdmi);
|
|
|
|
ret = devm_request_threaded_irq(dev, plat_data->main_irq,
|
|
dw_hdmi_qp_main_hardirq, NULL,
|
|
IRQF_SHARED, dev_name(dev), hdmi);
|
|
if (ret)
|
|
return ERR_PTR(ret);
|
|
|
|
hdmi->bridge.driver_private = hdmi;
|
|
hdmi->bridge.funcs = &dw_hdmi_qp_bridge_funcs;
|
|
hdmi->bridge.ops = DRM_BRIDGE_OP_DETECT |
|
|
DRM_BRIDGE_OP_EDID |
|
|
DRM_BRIDGE_OP_HDMI |
|
|
DRM_BRIDGE_OP_HPD;
|
|
hdmi->bridge.of_node = pdev->dev.of_node;
|
|
hdmi->bridge.type = DRM_MODE_CONNECTOR_HDMIA;
|
|
hdmi->bridge.vendor = "Synopsys";
|
|
hdmi->bridge.product = "DW HDMI QP TX";
|
|
|
|
hdmi->bridge.ddc = dw_hdmi_qp_i2c_adapter(hdmi);
|
|
if (IS_ERR(hdmi->bridge.ddc))
|
|
return ERR_CAST(hdmi->bridge.ddc);
|
|
|
|
ret = devm_drm_bridge_add(dev, &hdmi->bridge);
|
|
if (ret)
|
|
return ERR_PTR(ret);
|
|
|
|
ret = drm_bridge_attach(encoder, &hdmi->bridge, NULL,
|
|
DRM_BRIDGE_ATTACH_NO_CONNECTOR);
|
|
if (ret)
|
|
return ERR_PTR(ret);
|
|
|
|
return hdmi;
|
|
}
|
|
EXPORT_SYMBOL_GPL(dw_hdmi_qp_bind);
|
|
|
|
void dw_hdmi_qp_resume(struct device *dev, struct dw_hdmi_qp *hdmi)
|
|
{
|
|
dw_hdmi_qp_init_hw(hdmi);
|
|
}
|
|
EXPORT_SYMBOL_GPL(dw_hdmi_qp_resume);
|
|
|
|
MODULE_AUTHOR("Algea Cao <algea.cao@rock-chips.com>");
|
|
MODULE_AUTHOR("Cristian Ciocaltea <cristian.ciocaltea@collabora.com>");
|
|
MODULE_DESCRIPTION("DW HDMI QP transmitter library");
|
|
MODULE_LICENSE("GPL");
|