summaryrefslogtreecommitdiffstats
path: root/drivers
diff options
context:
space:
mode:
authorSwapnil Jakhade <sjakhade@cadence.com>2020-09-18 14:09:22 +0200
committerTomi Valkeinen <tomi.valkeinen@ti.com>2020-09-18 15:16:01 +0300
commitfb43aa0acdfd600c75b8c877bdf9f6e9893ffc9b (patch)
treee1ac6036688d8f369a8a75373b7b4c0ab72a38f5 /drivers
parent85649cc8dc509dfb97f5ac87f7efefe03539323a (diff)
drm: bridge: Add support for Cadence MHDP8546 DPI/DP bridge
Add a new DRM bridge driver for Cadence MHDP8546 DPTX IP used in TI J721E SoC. MHDP DPTX IP is the component that complies with VESA DisplayPort (DP) and embedded Display Port (eDP) standards. It integrates uCPU running the embedded Firmware (FW) interfaced over APB interface. Basically, it takes a DPI stream as input and outputs it encoded in DP format. Currently, it supports only SST mode. Co-developed-by: Tomi Valkeinen <tomi.valkeinen@ti.com> Co-developed-by: Jyri Sarha <jsarha@ti.com> Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ti.com> Signed-off-by: Jyri Sarha <jsarha@ti.com> Signed-off-by: Quentin Schulz <quentin.schulz@free-electrons.com> Signed-off-by: Yuti Amonkar <yamonkar@cadence.com> Signed-off-by: Swapnil Jakhade <sjakhade@cadence.com> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ti.com>
Diffstat (limited to 'drivers')
-rw-r--r--drivers/gpu/drm/bridge/Kconfig2
-rw-r--r--drivers/gpu/drm/bridge/Makefile1
-rw-r--r--drivers/gpu/drm/bridge/cadence/Kconfig11
-rw-r--r--drivers/gpu/drm/bridge/cadence/Makefile3
-rw-r--r--drivers/gpu/drm/bridge/cadence/cdns-mhdp8546-core.c2522
-rw-r--r--drivers/gpu/drm/bridge/cadence/cdns-mhdp8546-core.h399
6 files changed, 2938 insertions, 0 deletions
diff --git a/drivers/gpu/drm/bridge/Kconfig b/drivers/gpu/drm/bridge/Kconfig
index 3e11af4e9f63..ef91646441b1 100644
--- a/drivers/gpu/drm/bridge/Kconfig
+++ b/drivers/gpu/drm/bridge/Kconfig
@@ -241,6 +241,8 @@ source "drivers/gpu/drm/bridge/analogix/Kconfig"
source "drivers/gpu/drm/bridge/adv7511/Kconfig"
+source "drivers/gpu/drm/bridge/cadence/Kconfig"
+
source "drivers/gpu/drm/bridge/synopsys/Kconfig"
endmenu
diff --git a/drivers/gpu/drm/bridge/Makefile b/drivers/gpu/drm/bridge/Makefile
index c589a6a7cbe1..2b3aff104e46 100644
--- a/drivers/gpu/drm/bridge/Makefile
+++ b/drivers/gpu/drm/bridge/Makefile
@@ -25,4 +25,5 @@ obj-$(CONFIG_DRM_TI_TPD12S015) += ti-tpd12s015.o
obj-$(CONFIG_DRM_NWL_MIPI_DSI) += nwl-dsi.o
obj-y += analogix/
+obj-y += cadence/
obj-y += synopsys/
diff --git a/drivers/gpu/drm/bridge/cadence/Kconfig b/drivers/gpu/drm/bridge/cadence/Kconfig
new file mode 100644
index 000000000000..f49d77eb7814
--- /dev/null
+++ b/drivers/gpu/drm/bridge/cadence/Kconfig
@@ -0,0 +1,11 @@
+# SPDX-License-Identifier: GPL-2.0-only
+config DRM_CDNS_MHDP8546
+ tristate "Cadence DPI/DP bridge"
+ select DRM_KMS_HELPER
+ select DRM_PANEL_BRIDGE
+ depends on OF
+ help
+ Support Cadence DPI to DP bridge. This is an internal
+ bridge and is meant to be directly embedded in a SoC.
+ It takes a DPI stream as input and outputs it encoded
+ in DP format.
diff --git a/drivers/gpu/drm/bridge/cadence/Makefile b/drivers/gpu/drm/bridge/cadence/Makefile
new file mode 100644
index 000000000000..676739cdf5e6
--- /dev/null
+++ b/drivers/gpu/drm/bridge/cadence/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0-only
+obj-$(CONFIG_DRM_CDNS_MHDP8546) += cdns-mhdp8546.o
+cdns-mhdp8546-y := cdns-mhdp8546-core.o
diff --git a/drivers/gpu/drm/bridge/cadence/cdns-mhdp8546-core.c b/drivers/gpu/drm/bridge/cadence/cdns-mhdp8546-core.c
new file mode 100644
index 000000000000..7b1bd5d10923
--- /dev/null
+++ b/drivers/gpu/drm/bridge/cadence/cdns-mhdp8546-core.c
@@ -0,0 +1,2522 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Cadence MHDP8546 DP bridge driver.
+ *
+ * Copyright (C) 2020 Cadence Design Systems, Inc.
+ *
+ * Authors: Quentin Schulz <quentin.schulz@free-electrons.com>
+ * Swapnil Jakhade <sjakhade@cadence.com>
+ * Yuti Amonkar <yamonkar@cadence.com>
+ * Tomi Valkeinen <tomi.valkeinen@ti.com>
+ * Jyri Sarha <jsarha@ti.com>
+ *
+ * TODO:
+ * - Implement optimized mailbox communication using mailbox interrupts
+ * - Add support for power management
+ * - Add support for features like audio, MST and fast link training
+ * - Implement request_fw_cancel to handle HW_STATE
+ * - Fix asynchronous loading of firmware implementation
+ * - Add DRM helper function for cdns_mhdp_lower_link_rate
+ */
+
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/err.h>
+#include <linux/firmware.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
+#include <linux/irq.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/of_device.h>
+#include <linux/phy/phy.h>
+#include <linux/phy/phy-dp.h>
+#include <linux/platform_device.h>
+#include <linux/slab.h>
+#include <linux/wait.h>
+
+#include <drm/drm_atomic.h>
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_atomic_state_helper.h>
+#include <drm/drm_bridge.h>
+#include <drm/drm_connector.h>
+#include <drm/drm_crtc_helper.h>
+#include <drm/drm_dp_helper.h>
+#include <drm/drm_modeset_helper_vtables.h>
+#include <drm/drm_print.h>
+#include <drm/drm_probe_helper.h>
+
+#include <asm/unaligned.h>
+
+#include "cdns-mhdp8546-core.h"
+
+static int cdns_mhdp_mailbox_read(struct cdns_mhdp_device *mhdp)
+{
+ int ret, empty;
+
+ WARN_ON(!mutex_is_locked(&mhdp->mbox_mutex));
+
+ ret = readx_poll_timeout(readl, mhdp->regs + CDNS_MAILBOX_EMPTY,
+ empty, !empty, MAILBOX_RETRY_US,
+ MAILBOX_TIMEOUT_US);
+ if (ret < 0)
+ return ret;
+
+ return readl(mhdp->regs + CDNS_MAILBOX_RX_DATA) & 0xff;
+}
+
+static int cdns_mhdp_mailbox_write(struct cdns_mhdp_device *mhdp, u8 val)
+{
+ int ret, full;
+
+ WARN_ON(!mutex_is_locked(&mhdp->mbox_mutex));
+
+ ret = readx_poll_timeout(readl, mhdp->regs + CDNS_MAILBOX_FULL,
+ full, !full, MAILBOX_RETRY_US,
+ MAILBOX_TIMEOUT_US);
+ if (ret < 0)
+ return ret;
+
+ writel(val, mhdp->regs + CDNS_MAILBOX_TX_DATA);
+
+ return 0;
+}
+
+static int cdns_mhdp_mailbox_recv_header(struct cdns_mhdp_device *mhdp,
+ u8 module_id, u8 opcode,
+ u16 req_size)
+{
+ u32 mbox_size, i;
+ u8 header[4];
+ int ret;
+
+ /* read the header of the message */
+ for (i = 0; i < sizeof(header); i++) {
+ ret = cdns_mhdp_mailbox_read(mhdp);
+ if (ret < 0)
+ return ret;
+
+ header[i] = ret;
+ }
+
+ mbox_size = get_unaligned_be16(header + 2);
+
+ if (opcode != header[0] || module_id != header[1] ||
+ req_size != mbox_size) {
+ /*
+ * If the message in mailbox is not what we want, we need to
+ * clear the mailbox by reading its contents.
+ */
+ for (i = 0; i < mbox_size; i++)
+ if (cdns_mhdp_mailbox_read(mhdp) < 0)
+ break;
+
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int cdns_mhdp_mailbox_recv_data(struct cdns_mhdp_device *mhdp,
+ u8 *buff, u16 buff_size)
+{
+ u32 i;
+ int ret;
+
+ for (i = 0; i < buff_size; i++) {
+ ret = cdns_mhdp_mailbox_read(mhdp);
+ if (ret < 0)
+ return ret;
+
+ buff[i] = ret;
+ }
+
+ return 0;
+}
+
+static int cdns_mhdp_mailbox_send(struct cdns_mhdp_device *mhdp, u8 module_id,
+ u8 opcode, u16 size, u8 *message)
+{
+ u8 header[4];
+ int ret, i;
+
+ header[0] = opcode;
+ header[1] = module_id;
+ put_unaligned_be16(size, header + 2);
+
+ for (i = 0; i < sizeof(header); i++) {
+ ret = cdns_mhdp_mailbox_write(mhdp, header[i]);
+ if (ret)
+ return ret;
+ }
+
+ for (i = 0; i < size; i++) {
+ ret = cdns_mhdp_mailbox_write(mhdp, message[i]);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+static
+int cdns_mhdp_reg_read(struct cdns_mhdp_device *mhdp, u32 addr, u32 *value)
+{
+ u8 msg[4], resp[8];
+ int ret;
+
+ put_unaligned_be32(addr, msg);
+
+ mutex_lock(&mhdp->mbox_mutex);
+
+ ret = cdns_mhdp_mailbox_send(mhdp, MB_MODULE_ID_GENERAL,
+ GENERAL_REGISTER_READ,
+ sizeof(msg), msg);
+ if (ret)
+ goto out;
+
+ ret = cdns_mhdp_mailbox_recv_header(mhdp, MB_MODULE_ID_GENERAL,
+ GENERAL_REGISTER_READ,
+ sizeof(resp));
+ if (ret)
+ goto out;
+
+ ret = cdns_mhdp_mailbox_recv_data(mhdp, resp, sizeof(resp));
+ if (ret)
+ goto out;
+
+ /* Returned address value should be the same as requested */
+ if (memcmp(msg, resp, sizeof(msg))) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ *value = get_unaligned_be32(resp + 4);
+
+out:
+ mutex_unlock(&mhdp->mbox_mutex);
+ if (ret) {
+ dev_err(mhdp->dev, "Failed to read register\n");
+ *value = 0;
+ }
+
+ return ret;
+}
+
+static
+int cdns_mhdp_reg_write(struct cdns_mhdp_device *mhdp, u16 addr, u32 val)
+{
+ u8 msg[6];
+ int ret;
+
+ put_unaligned_be16(addr, msg);
+ put_unaligned_be32(val, msg + 2);
+
+ mutex_lock(&mhdp->mbox_mutex);
+
+ ret = cdns_mhdp_mailbox_send(mhdp, MB_MODULE_ID_DP_TX,
+ DPTX_WRITE_REGISTER, sizeof(msg), msg);
+
+ mutex_unlock(&mhdp->mbox_mutex);
+
+ return ret;
+}
+
+static
+int cdns_mhdp_reg_write_bit(struct cdns_mhdp_device *mhdp, u16 addr,
+ u8 start_bit, u8 bits_no, u32 val)
+{
+ u8 field[8];
+ int ret;
+
+ put_unaligned_be16(addr, field);
+ field[2] = start_bit;
+ field[3] = bits_no;
+ put_unaligned_be32(val, field + 4);
+
+ mutex_lock(&mhdp->mbox_mutex);
+
+ ret = cdns_mhdp_mailbox_send(mhdp, MB_MODULE_ID_DP_TX,
+ DPTX_WRITE_FIELD, sizeof(field), field);
+
+ mutex_unlock(&mhdp->mbox_mutex);
+
+ return ret;
+}
+
+static
+int cdns_mhdp_dpcd_read(struct cdns_mhdp_device *mhdp,
+ u32 addr, u8 *data, u16 len)
+{
+ u8 msg[5], reg[5];
+ int ret;
+
+ put_unaligned_be16(len, msg);
+ put_unaligned_be24(addr, msg + 2);
+
+ mutex_lock(&mhdp->mbox_mutex);
+
+ ret = cdns_mhdp_mailbox_send(mhdp, MB_MODULE_ID_DP_TX,
+ DPTX_READ_DPCD, sizeof(msg), msg);
+ if (ret)
+ goto out;
+
+ ret = cdns_mhdp_mailbox_recv_header(mhdp, MB_MODULE_ID_DP_TX,
+ DPTX_READ_DPCD,
+ sizeof(reg) + len);
+ if (ret)
+ goto out;
+
+ ret = cdns_mhdp_mailbox_recv_data(mhdp, reg, sizeof(reg));
+ if (ret)
+ goto out;
+
+ ret = cdns_mhdp_mailbox_recv_data(mhdp, data, len);
+
+out:
+ mutex_unlock(&mhdp->mbox_mutex);
+
+ return ret;
+}
+
+static
+int cdns_mhdp_dpcd_write(struct cdns_mhdp_device *mhdp, u32 addr, u8 value)
+{
+ u8 msg[6], reg[5];
+ int ret;
+
+ put_unaligned_be16(1, msg);
+ put_unaligned_be24(addr, msg + 2);
+ msg[5] = value;
+
+ mutex_lock(&mhdp->mbox_mutex);
+
+ ret = cdns_mhdp_mailbox_send(mhdp, MB_MODULE_ID_DP_TX,
+ DPTX_WRITE_DPCD, sizeof(msg), msg);
+ if (ret)
+ goto out;
+
+ ret = cdns_mhdp_mailbox_recv_header(mhdp, MB_MODULE_ID_DP_TX,
+ DPTX_WRITE_DPCD, sizeof(reg));
+ if (ret)
+ goto out;
+
+ ret = cdns_mhdp_mailbox_recv_data(mhdp, reg, sizeof(reg));
+ if (ret)
+ goto out;
+
+ if (addr != get_unaligned_be24(reg + 2))
+ ret = -EINVAL;
+
+out:
+ mutex_unlock(&mhdp->mbox_mutex);
+
+ if (ret)
+ dev_err(mhdp->dev, "dpcd write failed: %d\n", ret);
+ return ret;
+}
+
+static
+int cdns_mhdp_set_firmware_active(struct cdns_mhdp_device *mhdp, bool enable)
+{
+ u8 msg[5];
+ int ret, i;
+
+ msg[0] = GENERAL_MAIN_CONTROL;
+ msg[1] = MB_MODULE_ID_GENERAL;
+ msg[2] = 0;
+ msg[3] = 1;
+ msg[4] = enable ? FW_ACTIVE : FW_STANDBY;
+
+ mutex_lock(&mhdp->mbox_mutex);
+
+ for (i = 0; i < sizeof(msg); i++) {
+ ret = cdns_mhdp_mailbox_write(mhdp, msg[i]);
+ if (ret)
+ goto out;
+ }
+
+ /* read the firmware state */
+ ret = cdns_mhdp_mailbox_recv_data(mhdp, msg, sizeof(msg));
+ if (ret)
+ goto out;
+
+ ret = 0;
+
+out:
+ mutex_unlock(&mhdp->mbox_mutex);
+
+ if (ret < 0)
+ dev_err(mhdp->dev, "set firmware active failed\n");
+ return ret;
+}
+
+static
+int cdns_mhdp_get_hpd_status(struct cdns_mhdp_device *mhdp)
+{
+ u8 status;
+ int ret;
+
+ mutex_lock(&mhdp->mbox_mutex);
+
+ ret = cdns_mhdp_mailbox_send(mhdp, MB_MODULE_ID_DP_TX,
+ DPTX_HPD_STATE, 0, NULL);
+ if (ret)
+ goto err_get_hpd;
+
+ ret = cdns_mhdp_mailbox_recv_header(mhdp, MB_MODULE_ID_DP_TX,
+ DPTX_HPD_STATE,
+ sizeof(status));
+ if (ret)
+ goto err_get_hpd;
+
+ ret = cdns_mhdp_mailbox_recv_data(mhdp, &status, sizeof(status));
+ if (ret)
+ goto err_get_hpd;
+
+ mutex_unlock(&mhdp->mbox_mutex);
+
+ dev_dbg(mhdp->dev, "%s: HPD %splugged\n", __func__,
+ status ? "" : "un");
+
+ return status;
+
+err_get_hpd:
+ mutex_unlock(&mhdp->mbox_mutex);
+
+ return ret;
+}
+
+static
+int cdns_mhdp_get_edid_block(void *data, u8 *edid,
+ unsigned int block, size_t length)
+{
+ struct cdns_mhdp_device *mhdp = data;
+ u8 msg[2], reg[2], i;
+ int ret;
+
+ mutex_lock(&mhdp->mbox_mutex);
+
+ for (i = 0; i < 4; i++) {
+ msg[0] = block / 2;
+ msg[1] = block % 2;
+
+ ret = cdns_mhdp_mailbox_send(mhdp, MB_MODULE_ID_DP_TX,
+ DPTX_GET_EDID, sizeof(msg), msg);
+ if (ret)
+ continue;
+
+ ret = cdns_mhdp_mailbox_recv_header(mhdp, MB_MODULE_ID_DP_TX,
+ DPTX_GET_EDID,
+ sizeof(reg) + length);
+ if (ret)
+ continue;
+
+ ret = cdns_mhdp_mailbox_recv_data(mhdp, reg, sizeof(reg));
+ if (ret)
+ continue;
+
+ ret = cdns_mhdp_mailbox_recv_data(mhdp, edid, length);
+ if (ret)
+ continue;
+
+ if (reg[0] == length && reg[1] == block / 2)
+ break;
+ }
+
+ mutex_unlock(&mhdp->mbox_mutex);
+
+ if (ret)
+ dev_err(mhdp->dev, "get block[%d] edid failed: %d\n",
+ block, ret);
+
+ return ret;
+}
+
+static
+int cdns_mhdp_read_hpd_event(struct cdns_mhdp_device *mhdp)
+{
+ u8 event = 0;
+ int ret;
+
+ mutex_lock(&mhdp->mbox_mutex);
+
+ ret = cdns_mhdp_mailbox_send(mhdp, MB_MODULE_ID_DP_TX,
+ DPTX_READ_EVENT, 0, NULL);
+ if (ret)
+ goto out;
+
+ ret = cdns_mhdp_mailbox_recv_header(mhdp, MB_MODULE_ID_DP_TX,
+ DPTX_READ_EVENT, sizeof(event));
+ if (ret < 0)
+ goto out;
+
+ ret = cdns_mhdp_mailbox_recv_data(mhdp, &event, sizeof(event));
+out:
+ mutex_unlock(&mhdp->mbox_mutex);
+
+ if (ret < 0)
+ return ret;
+
+ dev_dbg(mhdp->dev, "%s: %s%s%s%s\n", __func__,
+ (event & DPTX_READ_EVENT_HPD_TO_HIGH) ? "TO_HIGH " : "",
+ (event & DPTX_READ_EVENT_HPD_TO_LOW) ? "TO_LOW " : "",
+ (event & DPTX_READ_EVENT_HPD_PULSE) ? "PULSE " : "",
+ (event & DPTX_READ_EVENT_HPD_STATE) ? "HPD_STATE " : "");
+
+ return event;
+}
+
+static
+int cdns_mhdp_adjust_lt(struct cdns_mhdp_device *mhdp, unsigned int nlanes,
+ unsigned int udelay, const u8 *lanes_data,
+ u8 link_status[DP_LINK_STATUS_SIZE])
+{
+ u8 payload[7];
+ u8 hdr[5]; /* For DPCD read response header */
+ u32 addr;
+ int ret;
+
+ if (nlanes != 4 && nlanes != 2 && nlanes != 1) {
+ dev_err(mhdp->dev, "invalid number of lanes: %u\n", nlanes);
+ ret = -EINVAL;
+ goto out;
+ }
+
+ payload[0] = nlanes;
+ put_unaligned_be16(udelay, payload + 1);
+ memcpy(payload + 3, lanes_data, nlanes);
+
+ mutex_lock(&mhdp->mbox_mutex);
+
+ ret = cdns_mhdp_mailbox_send(mhdp, MB_MODULE_ID_DP_TX,
+ DPTX_ADJUST_LT,
+ sizeof(payload), payload);
+ if (ret)
+ goto out;
+
+ /* Yes, read the DPCD read command response */
+ ret = cdns_mhdp_mailbox_recv_header(mhdp, MB_MODULE_ID_DP_TX,
+ DPTX_READ_DPCD,
+ sizeof(hdr) + DP_LINK_STATUS_SIZE);
+ if (ret)
+ goto out;
+
+ ret = cdns_mhdp_mailbox_recv_data(mhdp, hdr, sizeof(hdr));
+ if (ret)
+ goto out;
+
+ addr = get_unaligned_be24(hdr + 2);
+ if (addr != DP_LANE0_1_STATUS)
+ goto out;
+
+ ret = cdns_mhdp_mailbox_recv_data(mhdp, link_status,
+ DP_LINK_STATUS_SIZE);
+
+out:
+ mutex_unlock(&mhdp->mbox_mutex);
+
+ if (ret)
+ dev_err(mhdp->dev, "Failed to adjust Link Training.\n");
+
+ return ret;
+}
+
+/**
+ * cdns_mhdp_link_power_up() - power up a DisplayPort link
+ * @aux: DisplayPort AUX channel
+ * @link: pointer to a structure containing the link configuration
+ *
+ * Returns 0 on success or a negative error code on failure.
+ */
+static
+int cdns_mhdp_link_power_up(struct drm_dp_aux *aux, struct cdns_mhdp_link *link)
+{
+ u8 value;
+ int err;
+
+ /* DP_SET_POWER register is only available on DPCD v1.1 and later */
+ if (link->revision < 0x11)
+ return 0;
+
+ err = drm_dp_dpcd_readb(aux, DP_SET_POWER, &value);
+ if (err < 0)
+ return err;
+
+ value &= ~DP_SET_POWER_MASK;
+ value |= DP_SET_POWER_D0;
+
+ err = drm_dp_dpcd_writeb(aux, DP_SET_POWER, value);
+ if (err < 0)
+ return err;
+
+ /*
+ * According to the DP 1.1 specification, a "Sink Device must exit the
+ * power saving state within 1 ms" (Section 2.5.3.1, Table 5-52, "Sink
+ * Control Field" (register 0x600).
+ */
+ usleep_range(1000, 2000);
+
+ return 0;
+}
+
+/**
+ * cdns_mhdp_link_power_down() - power down a DisplayPort link
+ * @aux: DisplayPort AUX channel
+ * @link: pointer to a structure containing the link configuration
+ *
+ * Returns 0 on success or a negative error code on failure.
+ */
+static
+int cdns_mhdp_link_power_down(struct drm_dp_aux *aux,
+ struct cdns_mhdp_link *link)
+{
+ u8 value;
+ int err;
+
+ /* DP_SET_POWER register is only available on DPCD v1.1 and later */
+ if (link->revision < 0x11)
+ return 0;
+
+ err = drm_dp_dpcd_readb(aux, DP_SET_POWER, &value);
+ if (err < 0)
+ return err;
+
+ value &= ~DP_SET_POWER_MASK;
+ value |= DP_SET_POWER_D3;
+
+ err = drm_dp_dpcd_writeb(aux, DP_SET_POWER, value);
+ if (err < 0)
+ return err;
+
+ return 0;
+}
+
+/**
+ * cdns_mhdp_link_configure() - configure a DisplayPort link
+ * @aux: DisplayPort AUX channel
+ * @link: pointer to a structure containing the link configuration
+ *
+ * Returns 0 on success or a negative error code on failure.
+ */
+static
+int cdns_mhdp_link_configure(struct drm_dp_aux *aux,
+ struct cdns_mhdp_link *link)
+{
+ u8 values[2];
+ int err;
+
+ values[0] = drm_dp_link_rate_to_bw_code(link->rate);
+ values[1] = link->num_lanes;
+
+ if (link->capabilities & DP_LINK_CAP_ENHANCED_FRAMING)
+ values[1] |= DP_LANE_COUNT_ENHANCED_FRAME_EN;
+
+ err = drm_dp_dpcd_write(aux, DP_LINK_BW_SET, values, sizeof(values));
+ if (err < 0)
+ return err;
+
+ return 0;
+}
+
+static unsigned int cdns_mhdp_max_link_rate(struct cdns_mhdp_device *mhdp)
+{
+ return min(mhdp->host.link_rate, mhdp->sink.link_rate);
+}
+
+static u8 cdns_mhdp_max_num_lanes(struct cdns_mhdp_device *mhdp)
+{
+ return min(mhdp->sink.lanes_cnt, mhdp->host.lanes_cnt);
+}
+
+static u8 cdns_mhdp_eq_training_pattern_supported(struct cdns_mhdp_device *mhdp)
+{
+ return fls(mhdp->host.pattern_supp & mhdp->sink.pattern_supp);
+}
+
+static bool cdns_mhdp_get_ssc_supported(struct cdns_mhdp_device *mhdp)
+{
+ /* Check if SSC is supported by both sides */
+ return mhdp->host.ssc && mhdp->sink.ssc;
+}
+
+static enum drm_connector_status cdns_mhdp_detect(struct cdns_mhdp_device *mhdp)
+{
+ dev_dbg(mhdp->dev, "%s: %d\n", __func__, mhdp->plugged);
+
+ if (mhdp->plugged)
+ return connector_status_connected;
+ else
+ return connector_status_disconnected;
+}
+
+static int cdns_mhdp_check_fw_version(struct cdns_mhdp_device *mhdp)
+{
+ u32 major_num, minor_num, revision;
+ u32 fw_ver, lib_ver;
+
+ fw_ver = (readl(mhdp->regs + CDNS_VER_H) << 8)
+ | readl(mhdp->regs + CDNS_VER_L);
+
+ lib_ver = (readl(mhdp->regs + CDNS_LIB_H_ADDR) << 8)
+ | readl(mhdp->regs + CDNS_LIB_L_ADDR);
+
+ if (lib_ver < 33984) {
+ /*
+ * Older FW versions with major number 1, used to store FW
+ * version information by storing repository revision number
+ * in registers. This is for identifying these FW versions.
+ */
+ major_num = 1;
+ minor_num = 2;
+ if (fw_ver == 26098) {
+ revision = 15;
+ } else if (lib_ver == 0 && fw_ver == 0) {
+ revision = 17;
+ } else {
+ dev_err(mhdp->dev, "Unsupported FW version: fw_ver = %u, lib_ver = %u\n",
+ fw_ver, lib_ver);
+ return -ENODEV;
+ }
+ } else {
+ /* To identify newer FW versions with major number 2 onwards. */
+ major_num = fw_ver / 10000;
+ minor_num = (fw_ver / 100) % 100;
+ revision = (fw_ver % 10000) % 100;
+ }
+
+ dev_dbg(mhdp->dev, "FW version: v%u.%u.%u\n", major_num, minor_num,
+ revision);
+ return 0;
+}
+
+static int cdns_mhdp_fw_activate(const struct firmware *fw,
+ struct cdns_mhdp_device *mhdp)
+{
+ unsigned int reg;
+ int ret;
+
+ /* Release uCPU reset and stall it. */
+ writel(CDNS_CPU_STALL, mhdp->regs + CDNS_APB_CTRL);
+
+ memcpy_toio(mhdp->regs + CDNS_MHDP_IMEM, fw->data, fw->size);
+
+ /* Leave debug mode, release stall */
+ writel(0, mhdp->regs + CDNS_APB_CTRL);
+
+ /*
+ * Wait for the KEEP_ALIVE "message" on the first 8 bits.
+ * Updated each sched "tick" (~2ms)
+ */
+ ret = readl_poll_timeout(mhdp->regs + CDNS_KEEP_ALIVE, reg,
+ reg & CDNS_KEEP_ALIVE_MASK, 500,
+ CDNS_KEEP_ALIVE_TIMEOUT);
+ if (ret) {
+ dev_err(mhdp->dev,
+ "device didn't give any life sign: reg %d\n", reg);
+ return ret;
+ }
+
+ ret = cdns_mhdp_check_fw_version(mhdp);
+ if (ret)
+ return ret;
+
+ /* Init events to 0 as it's not cleared by FW at boot but on read */
+ readl(mhdp->regs + CDNS_SW_EVENT0);
+ readl(mhdp->regs + CDNS_SW_EVENT1);
+ readl(mhdp->regs + CDNS_SW_EVENT2);
+ readl(mhdp->regs + CDNS_SW_EVENT3);
+
+ /* Activate uCPU */
+ ret = cdns_mhdp_set_firmware_active(mhdp, true);
+ if (ret)
+ return ret;
+
+ spin_lock(&mhdp->start_lock);
+
+ mhdp->hw_state = MHDP_HW_READY;
+
+ /*
+ * Here we must keep the lock while enabling the interrupts
+ * since it would otherwise be possible that interrupt enable
+ * code is executed after the bridge is detached. The similar
+ * situation is not possible in attach()/detach() callbacks
+ * since the hw_state changes from MHDP_HW_READY to
+ * MHDP_HW_STOPPED happens only due to driver removal when
+ * bridge should already be detached.
+ */
+ if (mhdp->bridge_attached)
+ writel(~CDNS_APB_INT_MASK_SW_EVENT_INT,
+ mhdp->regs + CDNS_APB_INT_MASK);
+
+ spin_unlock(&mhdp->start_lock);
+
+ wake_up(&mhdp->fw_load_wq);
+ dev_dbg(mhdp->dev, "DP FW activated\n");
+
+ return 0;
+}
+
+static void cdns_mhdp_fw_cb(const struct firmware *fw, void *context)
+{
+ struct cdns_mhdp_device *mhdp = context;
+ bool bridge_attached;
+ int ret;
+
+ dev_dbg(mhdp->dev, "firmware callback\n");
+
+ if (!fw || !fw->data) {
+ dev_err(mhdp->dev, "%s: No firmware.\n", __func__);
+ return;
+ }
+
+ ret = cdns_mhdp_fw_activate(fw, mhdp);
+
+ release_firmware(fw);
+
+ if (ret)
+ return;
+
+ /*
+ * XXX how to make sure the bridge is still attached when
+ * calling drm_kms_helper_hotplug_event() after releasing
+ * the lock? We should not hold the spin lock when
+ * calling drm_kms_helper_hotplug_event() since it may
+ * cause a dead lock. FB-dev console calls detect from the
+ * same thread just down the call stack started here.
+ */
+ spin_lock(&mhdp->start_lock);
+ bridge_attached = mhdp->bridge_attached;
+ spin_unlock(&mhdp->start_lock);
+ if (bridge_attached) {
+ if (mhdp->connector.dev)
+ drm_kms_helper_hotplug_event(mhdp->bridge.dev);
+ else
+ drm_bridge_hpd_notify(&mhdp->bridge, cdns_mhdp_detect(mhdp));
+ }
+}
+
+static int cdns_mhdp_load_firmware(struct cdns_mhdp_device *mhdp)
+{
+ int ret;
+
+ ret = request_firmware_nowait(THIS_MODULE, true, FW_NAME, mhdp->dev,
+ GFP_KERNEL, mhdp, cdns_mhdp_fw_cb);
+ if (ret) {
+ dev_err(mhdp->dev, "failed to load firmware (%s), ret: %d\n",
+ FW_NAME, ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+static ssize_t cdns_mhdp_transfer(struct drm_dp_aux *aux,
+ struct drm_dp_aux_msg *msg)
+{
+ struct cdns_mhdp_device *mhdp = dev_get_drvdata(aux->dev);
+ int ret;
+
+ if (msg->request != DP_AUX_NATIVE_WRITE &&
+ msg->request != DP_AUX_NATIVE_READ)
+ return -EOPNOTSUPP;
+
+ if (msg->request == DP_AUX_NATIVE_WRITE) {
+ const u8 *buf = msg->buffer;
+ unsigned int i;
+
+ for (i = 0; i < msg->size; ++i) {
+ ret = cdns_mhdp_dpcd_write(mhdp,
+ msg->address + i, buf[i]);
+ if (!ret)
+ continue;
+
+ dev_err(mhdp->dev,
+ "Failed to write DPCD addr %u\n",
+ msg->address + i);
+
+ return ret;
+ }
+ } else {
+ ret = cdns_mhdp_dpcd_read(mhdp, msg->address,
+ msg->buffer, msg->size);
+ if (ret) {
+ dev_err(mhdp->dev,
+ "Failed to read DPCD addr %u\n",
+ msg->address);
+
+ return ret;
+ }
+ }
+
+ return msg->size;
+}
+
+static int cdns_mhdp_link_training_init(struct cdns_mhdp_device *mhdp)
+{
+ union phy_configure_opts phy_cfg;
+ u32 reg32;
+ int ret;
+
+ drm_dp_dpcd_writeb(&mhdp->aux, DP_TRAINING_PATTERN_SET,
+ DP_TRAINING_PATTERN_DISABLE);
+
+ /* Reset PHY configuration */
+ reg32 = CDNS_PHY_COMMON_CONFIG | CDNS_PHY_TRAINING_TYPE(1);
+ if (!mhdp->host.scrambler)
+ reg32 |= CDNS_PHY_SCRAMBLER_BYPASS;
+
+ cdns_mhdp_reg_write(mhdp, CDNS_DPTX_PHY_CONFIG, reg32);
+
+ cdns_mhdp_reg_write(mhdp, CDNS_DP_ENHNCD,
+ mhdp->sink.enhanced & mhdp->host.enhanced);
+
+ cdns_mhdp_reg_write(mhdp, CDNS_DP_LANE_EN,
+ CDNS_DP_LANE_EN_LANES(mhdp->link.num_lanes));
+
+ cdns_mhdp_link_configure(&mhdp->aux, &mhdp->link);
+ phy_cfg.dp.link_rate = mhdp->link.rate / 100;
+ phy_cfg.dp.lanes = mhdp->link.num_lanes;
+
+ memset(phy_cfg.dp.voltage, 0, sizeof(phy_cfg.dp.voltage));
+ memset(phy_cfg.dp.pre, 0, sizeof(phy_cfg.dp.pre));
+
+ phy_cfg.dp.ssc = cdns_mhdp_get_ssc_supported(mhdp);
+ phy_cfg.dp.set_lanes = true;
+ phy_cfg.dp.set_rate = true;
+ phy_cfg.dp.set_voltages = true;
+ ret = phy_configure(mhdp->phy, &phy_cfg);
+ if (ret) {
+ dev_err(mhdp->dev, "%s: phy_configure() failed: %d\n",
+ __func__, ret);
+ return ret;
+ }
+
+ cdns_mhdp_reg_write(mhdp, CDNS_DPTX_PHY_CONFIG,
+ CDNS_PHY_COMMON_CONFIG |
+ CDNS_PHY_TRAINING_EN |
+ CDNS_PHY_TRAINING_TYPE(1) |
+ CDNS_PHY_SCRAMBLER_BYPASS);
+
+ drm_dp_dpcd_writeb(&mhdp->aux, DP_TRAINING_PATTERN_SET,
+ DP_TRAINING_PATTERN_1 | DP_LINK_SCRAMBLING_DISABLE);
+
+ return 0;
+}
+
+static void cdns_mhdp_get_adjust_train(struct cdns_mhdp_device *mhdp,
+ u8 link_status[DP_LINK_STATUS_SIZE],
+ u8 lanes_data[CDNS_DP_MAX_NUM_LANES],
+ union phy_configure_opts *phy_cfg)
+{
+ u8 adjust, max_pre_emph, max_volt_swing;
+ u8 set_volt, set_pre;
+ unsigned int i;
+
+ max_pre_emph = CDNS_PRE_EMPHASIS(mhdp->host.pre_emphasis)
+ << DP_TRAIN_PRE_EMPHASIS_SHIFT;
+ max_volt_swing = CDNS_VOLT_SWING(mhdp->host.volt_swing);
+
+ for (i = 0; i < mhdp->link.num_lanes; i++) {
+ /* Check if Voltage swing and pre-emphasis are within limits */
+ adjust = drm_dp_get_adjust_request_voltage(link_status, i);
+ set_volt = min(adjust, max_volt_swing);
+
+ adjust = drm_dp_get_adjust_request_pre_emphasis(link_status, i);
+ set_pre = min(adjust, max_pre_emph)
+ >> DP_TRAIN_PRE_EMPHASIS_SHIFT;
+
+ /*
+ * Voltage swing level and pre-emphasis level combination is
+ * not allowed: leaving pre-emphasis as-is, and adjusting
+ * voltage swing.
+ */
+ if (set_volt + set_pre > 3)
+ set_volt = 3 - set_pre;
+
+ phy_cfg->dp.voltage[i] = set_volt;
+ lanes_data[i] = set_volt;
+
+ if (set_volt == max_volt_swing)
+ lanes_data[i] |= DP_TRAIN_MAX_SWING_REACHED;
+
+ phy_cfg->dp.pre[i] = set_pre;
+ lanes_data[i] |= (set_pre << DP_TRAIN_PRE_EMPHASIS_SHIFT);
+
+ if (set_pre == (max_pre_emph >> DP_TRAIN_PRE_EMPHASIS_SHIFT))
+ lanes_data[i] |= DP_TRAIN_MAX_PRE_EMPHASIS_REACHED;
+ }
+}
+
+static
+void cdns_mhdp_set_adjust_request_voltage(u8 link_status[DP_LINK_STATUS_SIZE],
+ unsigned int lane, u8 volt)
+{
+ unsigned int s = ((lane & 1) ?
+ DP_ADJUST_VOLTAGE_SWING_LANE1_SHIFT :
+ DP_ADJUST_VOLTAGE_SWING_LANE0_SHIFT);
+ unsigned int idx = DP_ADJUST_REQUEST_LANE0_1 - DP_LANE0_1_STATUS + (lane >> 1);
+
+ link_status[idx] &= ~(DP_ADJUST_VOLTAGE_SWING_LANE0_MASK << s);
+ link_status[idx] |= volt << s;
+}
+
+static
+void cdns_mhdp_set_adjust_request_pre_emphasis(u8 link_status[DP_LINK_STATUS_SIZE],
+ unsigned int lane, u8 pre_emphasis)
+{
+ unsigned int s = ((lane & 1) ?
+ DP_ADJUST_PRE_EMPHASIS_LANE1_SHIFT :
+ DP_ADJUST_PRE_EMPHASIS_LANE0_SHIFT);
+ unsigned int idx = DP_ADJUST_REQUEST_LANE0_1 - DP_LANE0_1_STATUS + (lane >> 1);
+
+ link_status[idx] &= ~(DP_ADJUST_PRE_EMPHASIS_LANE0_MASK << s);
+ link_status[idx] |= pre_emphasis << s;
+}
+
+static void cdns_mhdp_adjust_requested_eq(struct cdns_mhdp_device *mhdp,
+ u8 link_status[DP_LINK_STATUS_SIZE])
+{
+ u8 max_pre = CDNS_PRE_EMPHASIS(mhdp->host.pre_emphasis);
+ u8 max_volt = CDNS_VOLT_SWING(mhdp->host.volt_swing);
+ unsigned int i;
+ u8 volt, pre;
+
+ for (i = 0; i < mhdp->link.num_lanes; i++) {
+ volt = drm_dp_get_adjust_request_voltage(link_status, i);
+ pre = drm_dp_get_adjust_request_pre_emphasis(link_status, i);
+ if (volt + pre > 3)
+ cdns_mhdp_set_adjust_request_voltage(link_status, i,
+ 3 - pre);
+ if (mhdp->host.volt_swing & CDNS_FORCE_VOLT_SWING)
+ cdns_mhdp_set_adjust_request_voltage(link_status, i,
+ max_volt);
+ if (mhdp->host.pre_emphasis & CDNS_FORCE_PRE_EMPHASIS)
+ cdns_mhdp_set_adjust_request_pre_emphasis(link_status,
+ i, max_pre);
+ }
+}
+
+static void cdns_mhdp_print_lt_status(const char *prefix,
+ struct cdns_mhdp_device *mhdp,
+ union phy_configure_opts *phy_cfg)
+{
+ char vs[8] = "0/0/0/0";
+ char pe[8] = "0/0/0/0";
+ unsigned int i;
+
+ for (i = 0; i < mhdp->link.num_lanes; i++) {
+ vs[i * 2] = '0' + phy_cfg->dp.voltage[i];
+ pe[i * 2] = '0' + phy_cfg->dp.pre[i];
+ }
+
+ vs[i * 2 - 1] = '\0';
+ pe[i * 2 - 1] = '\0';
+
+ dev_dbg(mhdp->dev, "%s, %u lanes, %u Mbps, vs %s, pe %s\n",
+ prefix,
+ mhdp->link.num_lanes, mhdp->link.rate / 100,
+ vs, pe);
+}
+
+static bool cdns_mhdp_link_training_channel_eq(struct cdns_mhdp_device *mhdp,
+ u8 eq_tps,
+ unsigned int training_interval)
+{
+ u8 lanes_data[CDNS_DP_MAX_NUM_LANES], fail_counter_short = 0;
+ u8 link_status[DP_LINK_STATUS_SIZE];
+ union phy_configure_opts phy_cfg;
+ u32 reg32;
+ int ret;
+ bool r;
+
+ dev_dbg(mhdp->dev, "Starting EQ phase\n");
+
+ /* Enable link training TPS[eq_tps] in PHY */
+ reg32 = CDNS_PHY_COMMON_CONFIG | CDNS_PHY_TRAINING_EN |
+ CDNS_PHY_TRAINING_TYPE(eq_tps);
+ if (eq_tps != 4)
+ reg32 |= CDNS_PHY_SCRAMBLER_BYPASS;
+ cdns_mhdp_reg_write(mhdp, CDNS_DPTX_PHY_CONFIG, reg32);
+
+ drm_dp_dpcd_writeb(&mhdp->aux, DP_TRAINING_PATTERN_SET,
+ (eq_tps != 4) ? eq_tps | DP_LINK_SCRAMBLING_DISABLE :
+ CDNS_DP_TRAINING_PATTERN_4);
+
+ drm_dp_dpcd_read_link_status(&mhdp->aux, link_status);
+
+ do {
+ cdns_mhdp_get_adjust_train(mhdp, link_status, lanes_data,
+ &phy_cfg);
+ phy_cfg.dp.lanes = mhdp->link.num_lanes;
+ phy_cfg.dp.ssc = cdns_mhdp_get_ssc_supported(mhdp);
+ phy_cfg.dp.set_lanes = false;
+ phy_cfg.dp.set_rate = false;
+ phy_cfg.dp.set_voltages = true;
+ ret = phy_configure(mhdp->phy, &phy_cfg);
+ if (ret) {
+ dev_err(mhdp->dev, "%s: phy_configure() failed: %d\n",
+ __func__, ret);
+ goto err;
+ }
+
+ cdns_mhdp_adjust_lt(mhdp, mhdp->link.num_lanes,
+ training_interval, lanes_data, link_status);
+
+ r = drm_dp_clock_recovery_ok(link_status, mhdp->link.num_lanes);
+ if (!r)
+ goto err;
+
+ if (drm_dp_channel_eq_ok(link_status, mhdp->link.num_lanes)) {
+ cdns_mhdp_print_lt_status("EQ phase ok", mhdp,
+ &phy_cfg);
+ return true;
+ }
+
+ fail_counter_short++;
+
+ cdns_mhdp_adjust_requested_eq(mhdp, link_status);
+ } while (fail_counter_short < 5);
+
+err:
+ cdns_mhdp_print_lt_status("EQ phase failed", mhdp, &phy_cfg);
+
+ return false;
+}
+
+static void cdns_mhdp_adjust_requested_cr(struct cdns_mhdp_device *mhdp,
+ u8 link_status[DP_LINK_STATUS_SIZE],
+ u8 *req_volt, u8 *req_pre)
+{
+ const u8 max_volt = CDNS_VOLT_SWING(mhdp->ho