| // 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 <linux/workqueue.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; | |
| struct work_struct hpd_work; | |
| }; | |
| 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, | |
| struct drm_encoder *encoder, | |
| enum drm_bridge_attach_flags flags) | |
| { | |
| return flags & DRM_BRIDGE_ATTACH_NO_CONNECTOR ? 0 : -EINVAL; | |
| } | |
| static void display_connector_destroy(struct drm_bridge *bridge) | |
| { | |
| struct display_connector *conn = to_display_connector(bridge); | |
| i2c_put_adapter(conn->bridge.ddc); | |
| } | |
| 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 enum drm_connector_status | |
| display_connector_bridge_detect(struct drm_bridge *bridge, struct drm_connector *connector) | |
| { | |
| return display_connector_detect(bridge); | |
| } | |
| static void display_connector_hpd_enable(struct drm_bridge *bridge) | |
| { | |
| struct display_connector *conn = to_display_connector(bridge); | |
| enable_irq(conn->hpd_irq); | |
| if (conn->bridge.type == DRM_MODE_CONNECTOR_DisplayPort) | |
| schedule_work(&conn->hpd_work); | |
| } | |
| static void display_connector_hpd_disable(struct drm_bridge *bridge) | |
| { | |
| struct display_connector *conn = to_display_connector(bridge); | |
| if (conn->bridge.type == DRM_MODE_CONNECTOR_DisplayPort) | |
| cancel_work_sync(&conn->hpd_work); | |
| disable_irq(conn->hpd_irq); | |
| } | |
| static void display_connector_hpd_work(struct work_struct *work) | |
| { | |
| struct display_connector *conn = container_of(work, struct display_connector, hpd_work); | |
| struct drm_bridge *bridge = &conn->bridge; | |
| drm_bridge_hpd_notify(bridge, display_connector_detect(bridge)); | |
| } | |
| 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 __free(drm_bridge_put) = 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_obj(*out_bus_fmts); | |
| 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 __free(drm_bridge_put) = 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_obj(*in_bus_fmts); | |
| 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, | |
| .destroy = display_connector_destroy, | |
| .detect = display_connector_bridge_detect, | |
| .hpd_enable = display_connector_hpd_enable, | |
| .hpd_disable = display_connector_hpd_disable, | |
| .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_create_state = drm_atomic_helper_bridge_create_state, | |
| }; | |
| 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_drm_bridge_alloc(&pdev->dev, struct display_connector, bridge, | |
| &display_connector_bridge_funcs); | |
| if (IS_ERR(conn)) | |
| return PTR_ERR(conn); | |
| 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; | |
| if (type == DRM_MODE_CONNECTOR_HDMIA || | |
| type == DRM_MODE_CONNECTOR_DisplayPort) | |
| conn->bridge.ycbcr_420_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_NO_AUTOEN | | |
| 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.of_node = pdev->dev.of_node; | |
| if (conn->bridge.ddc) | |
| conn->bridge.ops |= DRM_BRIDGE_OP_EDID | |
| | DRM_BRIDGE_OP_DETECT; | |
| /* Detecting the monitor requires reading DPCD */ | |
| if (conn->hpd_gpio && type != DRM_MODE_CONNECTOR_DisplayPort) | |
| conn->bridge.ops |= DRM_BRIDGE_OP_DETECT; | |
| if (conn->hpd_irq >= 0) | |
| conn->bridge.ops |= DRM_BRIDGE_OP_HPD; | |
| if (conn->hpd_irq >= 0 && type == DRM_MODE_CONNECTOR_DisplayPort) | |
| INIT_WORK(&conn->hpd_work, display_connector_hpd_work); | |
| 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); | |
| } | |
| 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 = 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"); |