From 82ad20b4e31313c48fb205ee3a9c49dab7779a15 Mon Sep 17 00:00:00 2001
From: Dietmar Muscholik <dietmar.muscholik@seco.com>
Date: Wed, 19 Feb 2025 13:42:06 +0000
Subject: [PATCH] [DRIVER] Add driver for NXP PTN3460 DP/LVDS bridge on
 Qualcomm SoCs

The Qualcomm (e)DP driver does not support bridges.
Hence this is not really a bridge driver, that means there are no attach-
or enable-functions.
Instead the bridge is configured when the device is probed. After this the
hotplug-detect-gpio is pulled to high to fake to the (e)DP driver a nativ
(e)DP display has been powered on.
---
 .../display/bridge/seco-pn3460-qcom.txt       |  27 ++
 drivers/staging/Kconfig                       |   2 +
 drivers/staging/Makefile                      |   1 +
 drivers/staging/gpu/drm/bridge/Kconfig        |   8 +
 drivers/staging/gpu/drm/bridge/Makefile       |   1 +
 .../gpu/drm/bridge/seco-ptn3460-qcom.c        | 361 ++++++++++++++++++
 6 files changed, 400 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/display/bridge/seco-pn3460-qcom.txt
 create mode 100644 drivers/staging/gpu/drm/bridge/Kconfig
 create mode 100644 drivers/staging/gpu/drm/bridge/Makefile
 create mode 100644 drivers/staging/gpu/drm/bridge/seco-ptn3460-qcom.c

diff --git a/Documentation/devicetree/bindings/display/bridge/seco-pn3460-qcom.txt b/Documentation/devicetree/bindings/display/bridge/seco-pn3460-qcom.txt
new file mode 100644
index 0000000000000..d47464145f3d1
--- /dev/null
+++ b/Documentation/devicetree/bindings/display/bridge/seco-pn3460-qcom.txt
@@ -0,0 +1,27 @@
+Device tree bindings for NXP PTN3460 DP/LVDS bridge on Qualcomm SoCs
+********************************************************************
+
+Properties
+----------
+- compatible = "seco,ptn3460-qcom"
+- reg = <0x60>: The address on the i2c-bus.
+- powerdown-gpios = <phandle>: gpio for power-down
+- reset-gpios = <phandle>: gpio for reset
+- lvds-enable-gpios = <phandle>: gpio for enableing the panel
+- hpd-gpios = <phandle>: gpio for hotplug-detect
+- backlight = <phandle>: handle for backlight
+- edid-emulation = <int>: edid to emulate, defaults to 0
+- width  = <int>: panel width
+- height  = <int>: panel height
+- data-mapping = "vesa24" | "jeida-24" | "jeida-18"
+- panel-timing: panel timing, see display-timings.yaml
+
+If no panel-timing is defined edid-emulation selects one of the
+7 EDIDs preconfigured in the bridge.
+
+Required
+--------
+- compatible
+- reg
+- reset-gpios
+- hpd-gpios
diff --git a/drivers/staging/Kconfig b/drivers/staging/Kconfig
index 7cac0f2fe312e..9c01043ab0d10 100644
--- a/drivers/staging/Kconfig
+++ b/drivers/staging/Kconfig
@@ -80,4 +80,6 @@ source "drivers/staging/vme_user/Kconfig"
 
 source "drivers/staging/mfd/Kconfig"
 
+source "drivers/staging/gpu/drm/bridge/Kconfig"
+
 endif # STAGING
diff --git a/drivers/staging/Makefile b/drivers/staging/Makefile
index 97a931f5121ab..21ef7e25b917c 100644
--- a/drivers/staging/Makefile
+++ b/drivers/staging/Makefile
@@ -29,3 +29,4 @@ obj-$(CONFIG_XIL_AXIS_FIFO)	+= axis-fifo/
 obj-$(CONFIG_FIELDBUS_DEV)     += fieldbus/
 obj-$(CONFIG_QLGE)		+= qlge/
 obj-$(CONFIG_MFD_SECO_STM32)	+= mfd/
+obj-y				+= gpu/drm/bridge/
diff --git a/drivers/staging/gpu/drm/bridge/Kconfig b/drivers/staging/gpu/drm/bridge/Kconfig
new file mode 100644
index 0000000000000..8d215b29be492
--- /dev/null
+++ b/drivers/staging/gpu/drm/bridge/Kconfig
@@ -0,0 +1,8 @@
+config DRM_SECO_PTN3460_QCOM
+	tristate "NXP PTN3460 DP/LVDS bridge on Qualcomm SoCs"
+	depends on OF && DRM && I2C
+	help
+		Since Qualcomm (e)DP does not support bridges this driver pretends
+		to be not a bridge but a native (e)DP display.
+
+		If you use a PTN3460 on Qualcomm hardware say Y here.
diff --git a/drivers/staging/gpu/drm/bridge/Makefile b/drivers/staging/gpu/drm/bridge/Makefile
new file mode 100644
index 0000000000000..6bb1a737c65f6
--- /dev/null
+++ b/drivers/staging/gpu/drm/bridge/Makefile
@@ -0,0 +1 @@
+obj-$(CONFIG_DRM_SECO_PTN3460_QCOM) += seco-ptn3460-qcom.o
diff --git a/drivers/staging/gpu/drm/bridge/seco-ptn3460-qcom.c b/drivers/staging/gpu/drm/bridge/seco-ptn3460-qcom.c
new file mode 100644
index 0000000000000..4a807c1425d50
--- /dev/null
+++ b/drivers/staging/gpu/drm/bridge/seco-ptn3460-qcom.c
@@ -0,0 +1,361 @@
+/*****************************************************************************/
+/* SPDX-License-Identifier: (GPL-2.0+ OR MIT)                                */
+/*
+ * NXP PTN3460 DP/LVDS bridge on Qualcomm SoCs
+ *
+ * The Qualcomm (e)DP driver does not support bridges.
+ * Hence this is not really a bridge driver, that means there are no attach-
+ * or enable-functions.
+ * Instead the bridge is configured when the device is probed. After this the
+ * hotplug-detect-gpio is pulled to high to fake to the (e)DP driver a nativ
+ * (e)DP has been powered on.
+ *
+ * Copyright 2024 SECO NE    Dietmar Muscholik <dietmar.muscholik@seco.com>
+ *
+ *****************************************************************************/
+
+#include <linux/i2c.h>
+#include <linux/gpio/consumer.h>
+#include <linux/backlight.h>
+#include <linux/delay.h>
+#include <video/display_timing.h>
+#include <video/of_display_timing.h>
+#include <drm/drm_edid.h>
+#include <video/videomode.h>
+
+#define DRIVER_NAME "seco-ptn3460-qcom"
+
+#define EDIT_DEFAULT 0
+
+// bridge definitions
+// see https://www.nxp.com/docs/en/application-note/AN11128.pdf
+// section 5
+#define PTN3460_EDID_ADDR 0x0
+#define PTN3460_EDID_SELECT_ADDR 0x84
+#define PTN3460_EDID_SELECT 1
+#define PTN3460_EDID_ENABLE 1
+#define PTN3460_EDID_ACCESS_ADDR 0x85
+#define PTN3460_LVDS_CTRL_ADDR1 0x81
+#define PTN3460_LVDS_CTRL_ADDR2 0x82
+#define PTN3460_LVDS_CTRL_ADDR3 0x83
+#define PTN3460_LVDS_CTRL_DEFAULT 0x0b
+#define PTN3460_LVDS_CTRL_VESA24 (0 << 4)
+#define PTN3460_LVDS_CTRL_JEIDA24 (1 << 4)
+#define PTN3460_LVDS_CTRL_JEIDA18 (2 << 4)
+#define PTN3460_LVDS_CTRL_DUAL (1 << 3)
+#define PTN3460_LVDS_CTRL_DE_LOW (1 << 2)
+
+
+static unsigned char edid_init[EDID_LENGTH] = {
+	// copied from AN11128.pdf
+	0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, // header
+	0x3B, 0x10, 0x60, 0x34, // mfg_id, prod_code
+	0x00, 0x00, 0x00, 0x00, // serial
+	0x26, 0x15, 0x01, 0x03, // week, year, version, revision
+	0x68, 0x1E, 0x16, 0x78, 0xEE,
+	0x37, 0x25, 0x9E, 0x58, 0x4A, 0x97, 0x26, 0x19, 0x50, 0x54, // color
+	0xAD, 0xEE, 0x00, // established timings
+	0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, // standard timings
+	0x01, 0x01,	0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+	// preferred timing
+	0x64, 0x19, // pixel clock
+	0x00, 0x40, 0x41, 0x00, 0x26, 0x30, 0x18, 0x88,
+	0x36, 0x00, 0x30, 0xE4, 0x10, 0x00, 0x00, 0x18,
+	// detailed timing
+	0x00, 0x00, 0x00, 0xFD, 0x00, 0x32, 0x4C, 0x1E, // range limits
+	0x3F, 0x08, 0x00, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x00, 0x00, 0x00, 0xFC, 0x00, 0x41,	0x49, 0x4F, // product name
+	0x20, 0x50, 0x43, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x00, 0x00, 0x00, 0xFF,	0x00, 0x4E, 0x58, 0x50, // serial number
+	0x20, 0x50, 0x54, 0x4E, 0x33, 0x34, 0x36, 0x30, 0x20, 0x20,
+	0x00, 0xAF, // extension blocks, checksum
+};
+
+// container for dt stuff
+struct ptn3460 {
+	struct gpio_desc *gpio_powerdown_n;
+	struct gpio_desc *gpio_reset_n;
+	struct gpio_desc *gpio_lvds_en;
+	struct gpio_desc *gpio_hpd;
+	struct backlight_device *backlight;
+	u32 edid_emulation;
+	struct display_timing timing;
+	const char *mapping;
+	bool dual_lvds;
+	struct edid *edid;
+};
+
+
+/*
+  fill the detailed timing of an EDID with display timings read from
+  device tree
+  see https://glenwing.github.io/docs/VESA-EEDID-A2.pdf
+  Table 3.1 and 3.21
+ */
+static int edid_from_timing(struct edid *edid,
+							struct display_timing *timing,
+							unsigned int width,
+							unsigned int height)
+{
+	struct videomode vm;
+	struct detailed_timing *dt = edid->detailed_timings;
+	u32 htotal, vtotal;
+	unsigned char *p,c;
+	size_t n;
+
+	edid->width_cm = width / 10;
+	edid->height_cm = height / 10;
+
+	// just to save some typing
+	videomode_from_timing(timing, &vm);
+	htotal = vm.hactive + vm.hfront_porch + vm.hback_porch + vm.hsync_len;
+	vtotal = vm.vactive + vm.vfront_porch + vm.vback_porch + vm.vsync_len;
+
+	dt->pixel_clock = vm.pixelclock / 10000;
+
+	dt->data.pixel_data.hactive_lo = vm.hactive & 0xff;
+	dt->data.pixel_data.hblank_lo = (htotal - vm.hactive) & 0xff;
+	dt->data.pixel_data.hactive_hblank_hi = ((vm.hactive >> 4) & 0xf0) +
+		(((htotal - vm.hactive) >> 8) & 0x0f);
+
+	dt->data.pixel_data.vactive_lo = vm.vactive & 0xff;
+	dt->data.pixel_data.vblank_lo = (vtotal - vm.vactive) & 0xff;
+	dt->data.pixel_data.vactive_vblank_hi = ((vm.vactive >> 4) & 0xf0) +
+		(((vtotal - vm.vactive) >> 8) & 0x0f);
+
+	dt->data.pixel_data.hsync_offset_lo = vm.hfront_porch & 0xff;
+	dt->data.pixel_data.hsync_pulse_width_lo = vm.hsync_len & 0xff;
+	dt->data.pixel_data.vsync_offset_pulse_width_lo =
+		((vm.vfront_porch & 0x0f) << 4)	+ (vm.vsync_len & 0x0f);
+	// write-only-code, hope I never have to change it
+	dt->data.pixel_data.hsync_vsync_offset_pulse_width_hi =
+		((vm.hfront_porch & 0x0300) >> 2) +
+		((vm.hsync_len & 0x0300) >> 4) +
+		((vm.vfront_porch & 0x0030) >> 2) +
+		((vm.vsync_len & 0x0030) >> 4);
+
+	dt->data.pixel_data.width_mm_lo = width & 0xff;
+	dt->data.pixel_data.height_mm_lo = height & 0xff;
+	dt->data.pixel_data.width_height_mm_hi = ((width & 0xff00) >> 4) +
+		((height & 0xff00) >> 8);
+
+	// calc checksum
+	for(p = (unsigned char *)edid, c = n = 0;
+		n < sizeof(*edid) - 1;
+		c += p[n], n++);
+	p[n] = -c;
+
+	return 0;
+}
+
+
+// read the dt
+static int ptn3460_parse_dt(struct device *dev, struct ptn3460 *data)
+{
+	int ret = 0;
+	unsigned int width,height;
+
+	data->gpio_reset_n = devm_gpiod_get(dev, "reset", GPIOD_OUT_LOW);
+	if(IS_ERR(data->gpio_reset_n))
+	{
+		ret = PTR_ERR(data->gpio_reset_n);
+		dev_err(dev, "failed to request reset-gpio: %d\n", ret);
+		return ret;
+	}
+
+	data->gpio_hpd = devm_gpiod_get(dev, "hpd", GPIOD_OUT_LOW);
+	if(IS_ERR(data->gpio_hpd))
+	{
+		ret = PTR_ERR(data->gpio_hpd);
+		dev_err(dev, "failed to request hpd-gpio: %d\n", ret);
+		return ret;
+	}
+
+	data->gpio_powerdown_n = devm_gpiod_get_optional(dev, "powerdown",
+													 GPIOD_OUT_LOW);
+	if(IS_ERR(data->gpio_powerdown_n))
+		dev_warn(dev, "failed to request powerdown-gpio: %ld\n",
+				 PTR_ERR(data->gpio_powerdown_n));
+
+	data->gpio_lvds_en = devm_gpiod_get_optional(dev, "lvds-enable",
+												 GPIOD_OUT_LOW);
+	if(IS_ERR(data->gpio_lvds_en))
+		dev_warn(dev, "failed to request lvds-enable-gpio: %ld\n",
+				 PTR_ERR(data->gpio_lvds_en));
+
+	data->backlight = devm_of_find_backlight(dev);
+		dev_warn(dev, "failed to find backlight: %ld\n",
+				 PTR_ERR(data->backlight));
+
+	if(IS_ERR(data->backlight))
+	ret = of_property_read_u32(dev->of_node, "edid-emulation",
+							   &data->edid_emulation);
+	if(ret)
+	{
+		dev_warn(dev, "no edit-emulation, using default\n");
+		data->edid_emulation = EDIT_DEFAULT;
+	}
+
+	ret = of_property_read_u32(dev->of_node, "width-mm", &width);
+	if(ret)
+		dev_warn(dev, "failed to read display width: %d\n", ret);
+	ret = of_property_read_u32(dev->of_node, "height-mm", &height);
+	if(ret)
+		dev_warn(dev, "failed to read display height: %d\n", ret);
+
+	ret = of_get_display_timing(dev->of_node, "panel-timing", &data->timing);
+	if(ret == 0)
+	{
+		data->edid = devm_kmalloc(dev, sizeof(*data->edid), GFP_KERNEL);
+		if(!data->edid)
+			return -ENOMEM;
+
+		memcpy(data->edid, edid_init, sizeof(*data->edid));
+		edid_from_timing(data->edid, &data->timing, width, height);
+	}
+	else
+		dev_warn(dev, "Failed to read display-timings: %d\n", ret);
+
+	ret = of_property_read_string(dev->of_node, "data-mapping", &data->mapping);
+	if(ret)
+		dev_warn(dev, "Failed to read data-mapping: %d\n", ret);
+
+	data->dual_lvds = of_property_read_bool(dev->of_node, "dual-lvds");
+
+	return 0;
+}
+
+
+// configure the bridge
+static int ptn3460_configure(struct i2c_client *client, struct ptn3460 *data)
+{
+	char msg[EDID_LENGTH + 1];
+	int ret;
+
+	// write edid to device if defined
+	if(data->edid)
+	{
+		msg[0] = PTN3460_EDID_ACCESS_ADDR;
+		msg[1] = data->edid_emulation;
+		ret = i2c_master_send(client, msg, 2);
+		if(ret < 0)
+		{
+			dev_err(&client->dev, "Failed to set EDID address: %d\n", ret);
+			return ret;
+		}
+
+		msg[0] = PTN3460_EDID_ADDR;
+		memcpy(msg + 1, data->edid, EDID_LENGTH);
+		ret = i2c_master_send(client, msg, EDID_LENGTH + 1);
+		if(ret < 0)
+		{
+			dev_err(&client->dev, "Failed to send EDID: %d\n", ret);
+			return ret;
+		}
+	}
+
+	// select edid
+	msg[0] = PTN3460_EDID_SELECT_ADDR;
+	msg[1] = (data->edid_emulation << PTN3460_EDID_SELECT) |
+		PTN3460_EDID_ENABLE;
+	ret = i2c_master_send(client, msg, 2);
+	if(ret < 0)
+	{
+		dev_err(&client->dev, "Failed to select EDID: %d\n", ret);
+		return ret;
+	}
+
+	// configure the LVDS interface
+	msg[0] = PTN3460_LVDS_CTRL_ADDR1;
+	msg[1] = PTN3460_LVDS_CTRL_DEFAULT;
+	if(data->mapping)
+	{
+		if(!strcmp(data->mapping, "vesa-24"))
+			msg[1] += PTN3460_LVDS_CTRL_VESA24;
+		else if(!strcmp(data->mapping, "jeida-24"))
+			msg[1] += PTN3460_LVDS_CTRL_JEIDA24;
+		else if(!strcmp(data->mapping, "jeida-18"))
+			msg[1] += PTN3460_LVDS_CTRL_JEIDA18;
+	}
+	if(data->timing.flags & DISPLAY_FLAGS_DE_LOW)
+		msg[1] |= PTN3460_LVDS_CTRL_DE_LOW;
+	else
+		msg[1] &= ~PTN3460_LVDS_CTRL_DE_LOW;
+	if(data->dual_lvds)
+		msg[1] |= PTN3460_LVDS_CTRL_DUAL;
+	else
+		msg[1] &= ~PTN3460_LVDS_CTRL_DUAL;
+
+	ret = i2c_master_send(client, msg, 2);
+	if(ret < 0)
+	{
+		dev_err(&client->dev, "Failed to configure LVDS: %d\n", ret);
+		return ret;
+	}
+
+	return 0;
+}
+
+
+// probe the device
+static int ptn3460_probe(struct i2c_client *client)
+{
+	struct device *dev=&client->dev;
+	struct ptn3460 *data;
+	int ret = 0;
+
+	dev_info(dev, "%s\n", __FUNCTION__);
+	data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
+	if(!data)
+		return -ENOMEM;
+
+	ret = ptn3460_parse_dt(dev, data);
+	if(ret)
+		return ret;
+
+	gpiod_set_value_cansleep(data->gpio_reset_n, 0);
+	gpiod_set_value_cansleep(data->gpio_hpd, 0);
+	if(!IS_ERR_OR_NULL(data->gpio_powerdown_n))
+		gpiod_set_value_cansleep(data->gpio_powerdown_n, 1);
+	usleep_range(10, 20);
+	gpiod_set_value_cansleep(data->gpio_reset_n, 1);
+	msleep(90);
+	ret = ptn3460_configure(client, data);
+	if(ret)
+	{
+		gpiod_set_value_cansleep(data->gpio_reset_n, 0);
+		if(!IS_ERR_OR_NULL(data->gpio_powerdown_n))
+			gpiod_set_value_cansleep(data->gpio_powerdown_n, 0);
+		return ret;
+	}
+
+	msleep(90);
+	gpiod_set_value_cansleep(data->gpio_hpd, 1);
+	if(!IS_ERR_OR_NULL(data->gpio_lvds_en))
+		gpiod_set_value_cansleep(data->gpio_lvds_en, 1);
+
+	if(!IS_ERR_OR_NULL(data->backlight))
+		backlight_enable(data->backlight);
+
+	dev_info(dev, "%s: OK\n", __FUNCTION__);
+	return 0;
+}
+
+static struct of_device_id ptn3460_match_table[] = {
+	{ .compatible = "seco,ptn3460-qcom",},
+};
+
+static struct i2c_driver ptn3460_driver = {
+	.probe = ptn3460_probe,
+	.driver = {
+		.name = DRIVER_NAME,
+		.owner = THIS_MODULE,
+		.of_match_table = ptn3460_match_table,
+	},
+};
+
+module_i2c_driver(ptn3460_driver);
+
+MODULE_AUTHOR("Dietmar Muscholik <dietmar.muscholik@seco.com>");
+MODULE_DESCRIPTION("NXP PTN3460 DP/LVDS bridge on Qualcomm SoCs");
+MODULE_LICENSE("GPL v2");
-- 
GitLab