diff options
Diffstat (limited to 'drivers/gpu/drm/bridge/display-connector.c')
| -rw-r--r-- | drivers/gpu/drm/bridge/display-connector.c | 295 | 
1 files changed, 295 insertions, 0 deletions
diff --git a/drivers/gpu/drm/bridge/display-connector.c b/drivers/gpu/drm/bridge/display-connector.c new file mode 100644 index 000000000000..4d278573cdb9 --- /dev/null +++ b/drivers/gpu/drm/bridge/display-connector.c @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2019 Laurent Pinchart <laurent.pinchart@ideasonboard.com> + */ + +#include <linux/gpio/consumer.h> +#include <linux/i2c.h> +#include <linux/interrupt.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/of.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> + +#include <drm/drm_bridge.h> +#include <drm/drm_edid.h> + +struct display_connector { +	struct drm_bridge	bridge; + +	struct gpio_desc	*hpd_gpio; +	int			hpd_irq; +}; + +static inline struct display_connector * +to_display_connector(struct drm_bridge *bridge) +{ +	return container_of(bridge, struct display_connector, bridge); +} + +static int display_connector_attach(struct drm_bridge *bridge, +				    enum drm_bridge_attach_flags flags) +{ +	return flags & DRM_BRIDGE_ATTACH_NO_CONNECTOR ? 0 : -EINVAL; +} + +static enum drm_connector_status +display_connector_detect(struct drm_bridge *bridge) +{ +	struct display_connector *conn = to_display_connector(bridge); + +	if (conn->hpd_gpio) { +		if (gpiod_get_value_cansleep(conn->hpd_gpio)) +			return connector_status_connected; +		else +			return connector_status_disconnected; +	} + +	if (conn->bridge.ddc && drm_probe_ddc(conn->bridge.ddc)) +		return connector_status_connected; + +	switch (conn->bridge.type) { +	case DRM_MODE_CONNECTOR_DVIA: +	case DRM_MODE_CONNECTOR_DVID: +	case DRM_MODE_CONNECTOR_DVII: +	case DRM_MODE_CONNECTOR_HDMIA: +	case DRM_MODE_CONNECTOR_HDMIB: +		/* +		 * For DVI and HDMI connectors a DDC probe failure indicates +		 * that no cable is connected. +		 */ +		return connector_status_disconnected; + +	case DRM_MODE_CONNECTOR_Composite: +	case DRM_MODE_CONNECTOR_SVIDEO: +	case DRM_MODE_CONNECTOR_VGA: +	default: +		/* +		 * Composite and S-Video connectors have no other detection +		 * mean than the HPD GPIO. For VGA connectors, even if we have +		 * an I2C bus, we can't assume that the cable is disconnected +		 * if drm_probe_ddc fails, as some cables don't wire the DDC +		 * pins. +		 */ +		return connector_status_unknown; +	} +} + +static struct edid *display_connector_get_edid(struct drm_bridge *bridge, +					       struct drm_connector *connector) +{ +	struct display_connector *conn = to_display_connector(bridge); + +	return drm_get_edid(connector, conn->bridge.ddc); +} + +static const struct drm_bridge_funcs display_connector_bridge_funcs = { +	.attach = display_connector_attach, +	.detect = display_connector_detect, +	.get_edid = display_connector_get_edid, +}; + +static irqreturn_t display_connector_hpd_irq(int irq, void *arg) +{ +	struct display_connector *conn = arg; +	struct drm_bridge *bridge = &conn->bridge; + +	drm_bridge_hpd_notify(bridge, display_connector_detect(bridge)); + +	return IRQ_HANDLED; +} + +static int display_connector_probe(struct platform_device *pdev) +{ +	struct display_connector *conn; +	unsigned int type; +	const char *label; +	int ret; + +	conn = devm_kzalloc(&pdev->dev, sizeof(*conn), GFP_KERNEL); +	if (!conn) +		return -ENOMEM; + +	platform_set_drvdata(pdev, conn); + +	type = (uintptr_t)of_device_get_match_data(&pdev->dev); + +	/* Get the exact connector type. */ +	switch (type) { +	case DRM_MODE_CONNECTOR_DVII: { +		bool analog, digital; + +		analog = of_property_read_bool(pdev->dev.of_node, "analog"); +		digital = of_property_read_bool(pdev->dev.of_node, "digital"); +		if (analog && !digital) { +			conn->bridge.type = DRM_MODE_CONNECTOR_DVIA; +		} else if (!analog && digital) { +			conn->bridge.type = DRM_MODE_CONNECTOR_DVID; +		} else if (analog && digital) { +			conn->bridge.type = DRM_MODE_CONNECTOR_DVII; +		} else { +			dev_err(&pdev->dev, "DVI connector with no type\n"); +			return -EINVAL; +		} +		break; +	} + +	case DRM_MODE_CONNECTOR_HDMIA: { +		const char *hdmi_type; + +		ret = of_property_read_string(pdev->dev.of_node, "type", +					      &hdmi_type); +		if (ret < 0) { +			dev_err(&pdev->dev, "HDMI connector with no type\n"); +			return -EINVAL; +		} + +		if (!strcmp(hdmi_type, "a") || !strcmp(hdmi_type, "c") || +		    !strcmp(hdmi_type, "d") || !strcmp(hdmi_type, "e")) { +			conn->bridge.type = DRM_MODE_CONNECTOR_HDMIA; +		} else if (!strcmp(hdmi_type, "b")) { +			conn->bridge.type = DRM_MODE_CONNECTOR_HDMIB; +		} else { +			dev_err(&pdev->dev, +				"Unsupported HDMI connector type '%s'\n", +				hdmi_type); +			return -EINVAL; +		} + +		break; +	} + +	default: +		conn->bridge.type = type; +		break; +	} + +	/* All the supported connector types support interlaced modes. */ +	conn->bridge.interlace_allowed = true; + +	/* Get the optional connector label. */ +	of_property_read_string(pdev->dev.of_node, "label", &label); + +	/* +	 * Get the HPD GPIO for DVI and HDMI connectors. If the GPIO can provide +	 * edge interrupts, register an interrupt handler. +	 */ +	if (type == DRM_MODE_CONNECTOR_DVII || +	    type == DRM_MODE_CONNECTOR_HDMIA) { +		conn->hpd_gpio = devm_gpiod_get_optional(&pdev->dev, "hpd", +							 GPIOD_IN); +		if (IS_ERR(conn->hpd_gpio)) { +			if (PTR_ERR(conn->hpd_gpio) != -EPROBE_DEFER) +				dev_err(&pdev->dev, +					"Unable to retrieve HPD GPIO\n"); +			return PTR_ERR(conn->hpd_gpio); +		} + +		conn->hpd_irq = gpiod_to_irq(conn->hpd_gpio); +	} else { +		conn->hpd_irq = -EINVAL; +	} + +	if (conn->hpd_irq >= 0) { +		ret = devm_request_threaded_irq(&pdev->dev, conn->hpd_irq, +						NULL, display_connector_hpd_irq, +						IRQF_TRIGGER_RISING | +						IRQF_TRIGGER_FALLING | +						IRQF_ONESHOT, +						"HPD", conn); +		if (ret) { +			dev_info(&pdev->dev, +				 "Failed to request HPD edge interrupt, falling back to polling\n"); +			conn->hpd_irq = -EINVAL; +		} +	} + +	/* Retrieve the DDC I2C adapter for DVI, HDMI and VGA connectors. */ +	if (type == DRM_MODE_CONNECTOR_DVII || +	    type == DRM_MODE_CONNECTOR_HDMIA || +	    type == DRM_MODE_CONNECTOR_VGA) { +		struct device_node *phandle; + +		phandle = of_parse_phandle(pdev->dev.of_node, "ddc-i2c-bus", 0); +		if (phandle) { +			conn->bridge.ddc = of_get_i2c_adapter_by_node(phandle); +			of_node_put(phandle); +			if (!conn->bridge.ddc) +				return -EPROBE_DEFER; +		} else { +			dev_dbg(&pdev->dev, +				"No I2C bus specified, disabling EDID readout\n"); +		} +	} + +	conn->bridge.funcs = &display_connector_bridge_funcs; +	conn->bridge.of_node = pdev->dev.of_node; + +	if (conn->bridge.ddc) +		conn->bridge.ops |= DRM_BRIDGE_OP_EDID +				 |  DRM_BRIDGE_OP_DETECT; +	if (conn->hpd_gpio) +		conn->bridge.ops |= DRM_BRIDGE_OP_DETECT; +	if (conn->hpd_irq >= 0) +		conn->bridge.ops |= DRM_BRIDGE_OP_HPD; + +	dev_dbg(&pdev->dev, +		"Found %s display connector '%s' %s DDC bus and %s HPD GPIO (ops 0x%x)\n", +		drm_get_connector_type_name(conn->bridge.type), +		label ? label : "<unlabelled>", +		conn->bridge.ddc ? "with" : "without", +		conn->hpd_gpio ? "with" : "without", +		conn->bridge.ops); + +	drm_bridge_add(&conn->bridge); + +	return 0; +} + +static int display_connector_remove(struct platform_device *pdev) +{ +	struct display_connector *conn = platform_get_drvdata(pdev); + +	drm_bridge_remove(&conn->bridge); + +	if (!IS_ERR(conn->bridge.ddc)) +		i2c_put_adapter(conn->bridge.ddc); + +	return 0; +} + +static const struct of_device_id display_connector_match[] = { +	{ +		.compatible = "composite-video-connector", +		.data = (void *)DRM_MODE_CONNECTOR_Composite, +	}, { +		.compatible = "dvi-connector", +		.data = (void *)DRM_MODE_CONNECTOR_DVII, +	}, { +		.compatible = "hdmi-connector", +		.data = (void *)DRM_MODE_CONNECTOR_HDMIA, +	}, { +		.compatible = "svideo-connector", +		.data = (void *)DRM_MODE_CONNECTOR_SVIDEO, +	}, { +		.compatible = "vga-connector", +		.data = (void *)DRM_MODE_CONNECTOR_VGA, +	}, +	{}, +}; +MODULE_DEVICE_TABLE(of, display_connector_match); + +static struct platform_driver display_connector_driver = { +	.probe	= display_connector_probe, +	.remove	= display_connector_remove, +	.driver		= { +		.name		= "display-connector", +		.of_match_table	= display_connector_match, +	}, +}; +module_platform_driver(display_connector_driver); + +MODULE_AUTHOR("Laurent Pinchart <laurent.pinchart@ideasonboard.com>"); +MODULE_DESCRIPTION("Display connector driver"); +MODULE_LICENSE("GPL");  | 
