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