mirror of
				https://github.com/torvalds/linux.git
				synced 2025-11-04 10:40:15 +02:00 
			
		
		
		
	Prefer using the struct drm_edid based callback and functions. Signed-off-by: Jani Nikula <jani.nikula@intel.com> Reviewed-by: Neil Armstrong <neil.armstrong@linaro.org> Link: https://patchwork.freedesktop.org/patch/msgid/4e59a181e71e20158106868d1a6f7165cd9193e3.1706038510.git.jani.nikula@intel.com
		
			
				
	
	
		
			436 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			436 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
// 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/media-bus-format.h>
 | 
						|
#include <linux/module.h>
 | 
						|
#include <linux/mutex.h>
 | 
						|
#include <linux/of.h>
 | 
						|
#include <linux/platform_device.h>
 | 
						|
#include <linux/regulator/consumer.h>
 | 
						|
 | 
						|
#include <drm/drm_atomic_helper.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;
 | 
						|
 | 
						|
	struct regulator	*supply;
 | 
						|
	struct gpio_desc	*ddc_en;
 | 
						|
};
 | 
						|
 | 
						|
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 const struct drm_edid *display_connector_edid_read(struct drm_bridge *bridge,
 | 
						|
							  struct drm_connector *connector)
 | 
						|
{
 | 
						|
	struct display_connector *conn = to_display_connector(bridge);
 | 
						|
 | 
						|
	return drm_edid_read_ddc(connector, conn->bridge.ddc);
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
 * Since this bridge is tied to the connector, it acts like a passthrough,
 | 
						|
 * so concerning the output bus formats, either pass the bus formats from the
 | 
						|
 * previous bridge or return fallback data like done in the bridge function:
 | 
						|
 * drm_atomic_bridge_chain_select_bus_fmts().
 | 
						|
 * This supports negotiation if the bridge chain has all bits in place.
 | 
						|
 */
 | 
						|
static u32 *display_connector_get_output_bus_fmts(struct drm_bridge *bridge,
 | 
						|
					struct drm_bridge_state *bridge_state,
 | 
						|
					struct drm_crtc_state *crtc_state,
 | 
						|
					struct drm_connector_state *conn_state,
 | 
						|
					unsigned int *num_output_fmts)
 | 
						|
{
 | 
						|
	struct drm_bridge *prev_bridge = drm_bridge_get_prev_bridge(bridge);
 | 
						|
	struct drm_bridge_state *prev_bridge_state;
 | 
						|
 | 
						|
	if (!prev_bridge || !prev_bridge->funcs->atomic_get_output_bus_fmts) {
 | 
						|
		struct drm_connector *conn = conn_state->connector;
 | 
						|
		u32 *out_bus_fmts;
 | 
						|
 | 
						|
		*num_output_fmts = 1;
 | 
						|
		out_bus_fmts = kmalloc(sizeof(*out_bus_fmts), GFP_KERNEL);
 | 
						|
		if (!out_bus_fmts)
 | 
						|
			return NULL;
 | 
						|
 | 
						|
		if (conn->display_info.num_bus_formats &&
 | 
						|
		    conn->display_info.bus_formats)
 | 
						|
			out_bus_fmts[0] = conn->display_info.bus_formats[0];
 | 
						|
		else
 | 
						|
			out_bus_fmts[0] = MEDIA_BUS_FMT_FIXED;
 | 
						|
 | 
						|
		return out_bus_fmts;
 | 
						|
	}
 | 
						|
 | 
						|
	prev_bridge_state = drm_atomic_get_new_bridge_state(crtc_state->state,
 | 
						|
							    prev_bridge);
 | 
						|
 | 
						|
	return prev_bridge->funcs->atomic_get_output_bus_fmts(prev_bridge, prev_bridge_state,
 | 
						|
							      crtc_state, conn_state,
 | 
						|
							      num_output_fmts);
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
 * Since this bridge is tied to the connector, it acts like a passthrough,
 | 
						|
 * so concerning the input bus formats, either pass the bus formats from the
 | 
						|
 * previous bridge or MEDIA_BUS_FMT_FIXED (like select_bus_fmt_recursive())
 | 
						|
 * when atomic_get_input_bus_fmts is not supported.
 | 
						|
 * This supports negotiation if the bridge chain has all bits in place.
 | 
						|
 */
 | 
						|
static u32 *display_connector_get_input_bus_fmts(struct drm_bridge *bridge,
 | 
						|
					struct drm_bridge_state *bridge_state,
 | 
						|
					struct drm_crtc_state *crtc_state,
 | 
						|
					struct drm_connector_state *conn_state,
 | 
						|
					u32 output_fmt,
 | 
						|
					unsigned int *num_input_fmts)
 | 
						|
{
 | 
						|
	struct drm_bridge *prev_bridge = drm_bridge_get_prev_bridge(bridge);
 | 
						|
	struct drm_bridge_state *prev_bridge_state;
 | 
						|
 | 
						|
	if (!prev_bridge || !prev_bridge->funcs->atomic_get_input_bus_fmts) {
 | 
						|
		u32 *in_bus_fmts;
 | 
						|
 | 
						|
		*num_input_fmts = 1;
 | 
						|
		in_bus_fmts = kmalloc(sizeof(*in_bus_fmts), GFP_KERNEL);
 | 
						|
		if (!in_bus_fmts)
 | 
						|
			return NULL;
 | 
						|
 | 
						|
		in_bus_fmts[0] = MEDIA_BUS_FMT_FIXED;
 | 
						|
 | 
						|
		return in_bus_fmts;
 | 
						|
	}
 | 
						|
 | 
						|
	prev_bridge_state = drm_atomic_get_new_bridge_state(crtc_state->state,
 | 
						|
							    prev_bridge);
 | 
						|
 | 
						|
	return prev_bridge->funcs->atomic_get_input_bus_fmts(prev_bridge, prev_bridge_state,
 | 
						|
							     crtc_state, conn_state, output_fmt,
 | 
						|
							     num_input_fmts);
 | 
						|
}
 | 
						|
 | 
						|
static const struct drm_bridge_funcs display_connector_bridge_funcs = {
 | 
						|
	.attach = display_connector_attach,
 | 
						|
	.detect = display_connector_detect,
 | 
						|
	.edid_read = display_connector_edid_read,
 | 
						|
	.atomic_get_output_bus_fmts = display_connector_get_output_bus_fmts,
 | 
						|
	.atomic_get_input_bus_fmts = display_connector_get_input_bus_fmts,
 | 
						|
	.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,
 | 
						|
};
 | 
						|
 | 
						|
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_get_supply(struct platform_device *pdev,
 | 
						|
					struct display_connector *conn,
 | 
						|
					const char *name)
 | 
						|
{
 | 
						|
	conn->supply = devm_regulator_get_optional(&pdev->dev, name);
 | 
						|
 | 
						|
	if (conn->supply == ERR_PTR(-ENODEV))
 | 
						|
		conn->supply = NULL;
 | 
						|
 | 
						|
	return PTR_ERR_OR_ZERO(conn->supply);
 | 
						|
}
 | 
						|
 | 
						|
static int display_connector_probe(struct platform_device *pdev)
 | 
						|
{
 | 
						|
	struct display_connector *conn;
 | 
						|
	unsigned int type;
 | 
						|
	const char *label = NULL;
 | 
						|
	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, HDMI and DP connectors. If the GPIO can provide
 | 
						|
	 * edge interrupts, register an interrupt handler.
 | 
						|
	 */
 | 
						|
	if (type == DRM_MODE_CONNECTOR_DVII ||
 | 
						|
	    type == DRM_MODE_CONNECTOR_HDMIA ||
 | 
						|
	    type == DRM_MODE_CONNECTOR_DisplayPort) {
 | 
						|
		conn->hpd_gpio = devm_gpiod_get_optional(&pdev->dev, "hpd",
 | 
						|
							 GPIOD_IN);
 | 
						|
		if (IS_ERR(conn->hpd_gpio))
 | 
						|
			return dev_err_probe(&pdev->dev, PTR_ERR(conn->hpd_gpio),
 | 
						|
					     "Unable to retrieve HPD GPIO\n");
 | 
						|
 | 
						|
		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");
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	/* Get the DP PWR for DP connector. */
 | 
						|
	if (type == DRM_MODE_CONNECTOR_DisplayPort) {
 | 
						|
		int ret;
 | 
						|
 | 
						|
		ret = display_connector_get_supply(pdev, conn, "dp-pwr");
 | 
						|
		if (ret < 0)
 | 
						|
			return dev_err_probe(&pdev->dev, ret, "failed to get DP PWR regulator\n");
 | 
						|
	}
 | 
						|
 | 
						|
	/* enable DDC */
 | 
						|
	if (type == DRM_MODE_CONNECTOR_HDMIA) {
 | 
						|
		int ret;
 | 
						|
 | 
						|
		conn->ddc_en = devm_gpiod_get_optional(&pdev->dev, "ddc-en",
 | 
						|
						       GPIOD_OUT_HIGH);
 | 
						|
 | 
						|
		if (IS_ERR(conn->ddc_en)) {
 | 
						|
			dev_err(&pdev->dev, "Couldn't get ddc-en gpio\n");
 | 
						|
			return PTR_ERR(conn->ddc_en);
 | 
						|
		}
 | 
						|
 | 
						|
		ret = display_connector_get_supply(pdev, conn, "hdmi-pwr");
 | 
						|
		if (ret < 0)
 | 
						|
			return dev_err_probe(&pdev->dev, ret, "failed to get HDMI +5V Power regulator\n");
 | 
						|
	}
 | 
						|
 | 
						|
	if (conn->supply) {
 | 
						|
		ret = regulator_enable(conn->supply);
 | 
						|
		if (ret) {
 | 
						|
			dev_err(&pdev->dev, "failed to enable PWR regulator: %d\n", ret);
 | 
						|
			return ret;
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	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 void display_connector_remove(struct platform_device *pdev)
 | 
						|
{
 | 
						|
	struct display_connector *conn = platform_get_drvdata(pdev);
 | 
						|
 | 
						|
	if (conn->ddc_en)
 | 
						|
		gpiod_set_value(conn->ddc_en, 0);
 | 
						|
 | 
						|
	if (conn->supply)
 | 
						|
		regulator_disable(conn->supply);
 | 
						|
 | 
						|
	drm_bridge_remove(&conn->bridge);
 | 
						|
 | 
						|
	if (!IS_ERR(conn->bridge.ddc))
 | 
						|
		i2c_put_adapter(conn->bridge.ddc);
 | 
						|
}
 | 
						|
 | 
						|
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,
 | 
						|
	}, {
 | 
						|
		.compatible = "dp-connector",
 | 
						|
		.data = (void *)DRM_MODE_CONNECTOR_DisplayPort,
 | 
						|
	},
 | 
						|
	{},
 | 
						|
};
 | 
						|
MODULE_DEVICE_TABLE(of, display_connector_match);
 | 
						|
 | 
						|
static struct platform_driver display_connector_driver = {
 | 
						|
	.probe	= display_connector_probe,
 | 
						|
	.remove_new = 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");
 |