From patchwork Fri May 10 07:10:37 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Luca Ceresoli X-Patchwork-Id: 13660964 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from gabe.freedesktop.org (gabe.freedesktop.org [131.252.210.177]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.lore.kernel.org (Postfix) with ESMTPS id 1CAADC25B4F for ; Fri, 10 May 2024 07:11:38 +0000 (UTC) Received: from gabe.freedesktop.org (localhost [127.0.0.1]) by gabe.freedesktop.org (Postfix) with ESMTP id 2F42B10E5A5; Fri, 10 May 2024 07:11:36 +0000 (UTC) Authentication-Results: gabe.freedesktop.org; dkim=pass (2048-bit key; unprotected) header.d=bootlin.com header.i=@bootlin.com header.b="F2Te8tO0"; dkim-atps=neutral Received: from relay5-d.mail.gandi.net (relay5-d.mail.gandi.net [217.70.183.197]) by gabe.freedesktop.org (Postfix) with ESMTPS id 92D2A10E598 for ; Fri, 10 May 2024 07:11:34 +0000 (UTC) Received: by mail.gandi.net (Postfix) with ESMTPSA id 314391C0005; Fri, 10 May 2024 07:11:31 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=bootlin.com; s=gm1; t=1715325093; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=TZpOXHh7CnEM4D1u1tGQ38W2j3r5rDs/UQI+e6C0fvw=; b=F2Te8tO0aZtJiDUApinRMHGDhYOxwKArMEbr2aEa5S35/Na/t8AhS/UgBtgw6cbCiZftch yBZBjRAUqL1YvP8ilnJyR0DalepTGI7XjR0D+Xhfi523BOl8yfHg9apsp9R53tDcA5nwB9 9RSXDNU9F22TUhLj40ug0LgqLzTcWvFk7Q/ULfTI72Y15IJ9v0e7pcBru9bJ+Ix/R/dg71 8tu2g9lV8I/J7etZVFITONBeEZMJEjODI6ujK8qrSO8Y52cTBm0n7cBVREU88Xk32btfBQ oJXqC7wTHWnF9zMsRiIoeZ/l2pXdwH52de+Z+hnGDH+8Ghi6G+SAp/Ao3KP+7g== From: Luca Ceresoli Date: Fri, 10 May 2024 09:10:37 +0200 Subject: [PATCH v2 1/5] dt-bindings: connector: add GE SUNH hotplug addon connector MIME-Version: 1.0 Message-Id: <20240510-hotplug-drm-bridge-v2-1-ec32f2c66d56@bootlin.com> References: <20240510-hotplug-drm-bridge-v2-0-ec32f2c66d56@bootlin.com> In-Reply-To: <20240510-hotplug-drm-bridge-v2-0-ec32f2c66d56@bootlin.com> To: Rob Herring , Krzysztof Kozlowski , Conor Dooley , Andrzej Hajda , Neil Armstrong , Robert Foss , Laurent Pinchart , Jonas Karlman , Jernej Skrabec , Maarten Lankhorst , Maxime Ripard , Thomas Zimmermann , David Airlie , Daniel Vetter , Derek Kiernan , Dragan Cvetic , Arnd Bergmann , Greg Kroah-Hartman , Saravana Kannan Cc: Paul Kocialkowski , =?utf-8?q?Herv=C3=A9_Codina?= , Thomas Petazzoni , devicetree@vger.kernel.org, linux-kernel@vger.kernel.org, dri-devel@lists.freedesktop.org, Paul Kocialkowski , Luca Ceresoli X-Mailer: b4 0.13.0 X-GND-Sasl: luca.ceresoli@bootlin.com X-BeenThere: dri-devel@lists.freedesktop.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: Direct Rendering Infrastructure - Development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: dri-devel-bounces@lists.freedesktop.org Sender: "dri-devel" Add bindings for the GE SUNH add-on connector. This is a physical, hot-pluggable connector that allows to attach and detach at runtime an add-on adding peripherals on non-discoverable busses. Signed-off-by: Luca Ceresoli --- NOTE: the second and third examples fail 'make dt_binding_check' because they are example of DT overlay code -- I'm not aware of a way to validate overlay examples as of now This patch is new in v2. --- .../connector/ge,sunh-addon-connector.yaml | 197 +++++++++++++++++++++ MAINTAINERS | 5 + 2 files changed, 202 insertions(+) diff --git a/Documentation/devicetree/bindings/connector/ge,sunh-addon-connector.yaml b/Documentation/devicetree/bindings/connector/ge,sunh-addon-connector.yaml new file mode 100644 index 000000000000..c7ac62e5f2c9 --- /dev/null +++ b/Documentation/devicetree/bindings/connector/ge,sunh-addon-connector.yaml @@ -0,0 +1,197 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/connector/ge,sunh-addon-connector.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: GE SUNH hotplug add-on connector + +maintainers: + - Luca Ceresoli + +description: + Represent the physical connector present on GE SUNH devices that allows + to attach and detach at runtime an add-on adding peripherals on + non-discoverable busses. + + This connector has status GPIOs to notify the connection status to the + CPU and a reset GPIO to allow the CPU to reset all the peripherals on the + add-on. It also has a 4-lane MIPI DSI bus. + + Add-on removal can happen at any moment under user control and without + prior notice to the CPU, making all of its components not usable + anymore. Later on, the same or a different add-on model can be connected. + +properties: + compatible: + const: ge,sunh-addon-connector + + reset-gpios: + description: An output GPIO to reset the peripherals on the add-on. + maxItems: 1 + + plugged-gpios: + description: An input GPIO that is asserted if and only if an add-on is + physically connected. + maxItems: 1 + + powergood-gpios: + description: An input GPIO that is asserted if and only if power rails + on the add-on are stable. + maxItems: 1 + + ports: + $ref: /schemas/graph.yaml#/properties/ports + + description: OF graph bindings modeling the MIPI DSI bus across the + connector. The connector splits the video pipeline in a fixed part + and a removable part. + + The fixed part of the video pipeline includes all components up to + the display controller and 0 or more bridges. The removable part + includes any bridges and any other components up to the panel. + + properties: + port@0: + $ref: /schemas/graph.yaml#/properties/port + description: The MIPI DSI bus line from the CPU to the connector. + The remote-endpoint sub-node must point to the last non-removable + component of the video pipeline. + + port@1: + $ref: /schemas/graph.yaml#/properties/port + + description: The MIPI DSI bus line from the connector to the + add-on. The remote-endpoint sub-node must point to the first + add-on component of the video pipeline. As it describes the + hot-pluggable hardware, the endpoint node cannot be filled until + an add-on is detected, so this needs to be done by a device tree + overlay at runtime. + + required: + - port@0 + - port@1 + +required: + - compatible + +unevaluatedProperties: false + +examples: + # Main DTS describing the "main" board up to the connector + - | + / { + #include + + addon_connector: addon-connector { + compatible = "ge,sunh-addon-connector"; + reset-gpios = <&gpio1 1 GPIO_ACTIVE_LOW>; + plugged-gpios = <&gpio1 2 GPIO_ACTIVE_LOW>; + powergood-gpios = <&gpio1 3 GPIO_ACTIVE_HIGH>; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + + hotplug_conn_dsi_in: endpoint { + remote-endpoint = <&previous_bridge_out>; + }; + }; + + port@1 { + reg = <1>; + + hotplug_conn_dsi_out: endpoint { + // remote-endpoint to be added by overlay + }; + }; + }; + }; + }; + + # "base" overlay describing the common components on every add-on that + # are required to read the model ID + - | + &i2c1 { + #address-cells = <1>; + #size-cells = <0>; + + eeprom@50 { + compatible = "atmel,24c64"; + reg = <0x50>; + + nvmem-layout { + compatible = "fixed-layout"; + #address-cells = <1>; + #size-cells = <1>; + + addon_model_id: addon-model-id@400 { + reg = <0x400 0x1>; + }; + }; + }; + }; + + &addon_connector { + nvmem-cells = <&addon_model_id>; + nvmem-cell-names = "id"; + }; + + # Add-on-specific overlay describing all add-on components not in the + # "base" overlay + - | + &hotplug_conn_dsi_out { + remote-endpoint = <&addon_bridge_in>; + }; + + &i2c1 { + #address-cells = <1>; + #size-cells = <0>; + + dsi-lvds-bridge@42 { + compatible = "some-video-bridge"; + reg = <0x42>; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + + addon_bridge_in: endpoint { + remote-endpoint = <&hotplug_conn_dsi_out>; + data-lanes = <1 2 3 4>; + }; + }; + + port@1 { + reg = <1>; + + addon_bridge_out: endpoint { + remote-endpoint = <&panel_in>; + }; + }; + }; + }; + }; + + &{/} + { + panel { + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0{ + reg = <0>; + + panel_in: endpoint { + remote-endpoint = <&addon_bridge_out>; + }; + }; + }; + }; diff --git a/MAINTAINERS b/MAINTAINERS index ec0284125e8f..4955501217eb 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -9897,6 +9897,11 @@ S: Maintained F: Documentation/devicetree/bindings/iio/pressure/honeywell,mprls0025pa.yaml F: drivers/iio/pressure/mprls0025pa* +HOTPLUG CONNECTOR FOR GE SUNH ADDONS +M: Luca Ceresoli +S: Maintained +F: Documentation/devicetree/bindings/connector/ge,sunh-addon-connector.yaml + HP BIOSCFG DRIVER M: Jorge Lopez L: platform-driver-x86@vger.kernel.org From patchwork Fri May 10 07:10:38 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Luca Ceresoli X-Patchwork-Id: 13660965 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from gabe.freedesktop.org (gabe.freedesktop.org [131.252.210.177]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.lore.kernel.org (Postfix) with ESMTPS id 613D7C25B10 for ; Fri, 10 May 2024 07:11:40 +0000 (UTC) Received: from gabe.freedesktop.org (localhost [127.0.0.1]) by gabe.freedesktop.org (Postfix) with ESMTP id EFF4010E651; Fri, 10 May 2024 07:11:38 +0000 (UTC) Authentication-Results: gabe.freedesktop.org; dkim=pass (2048-bit key; unprotected) header.d=bootlin.com header.i=@bootlin.com header.b="S8MGmZjU"; dkim-atps=neutral Received: from relay5-d.mail.gandi.net (relay5-d.mail.gandi.net [217.70.183.197]) by gabe.freedesktop.org (Postfix) with ESMTPS id 6D00C10E5A9 for ; Fri, 10 May 2024 07:11:36 +0000 (UTC) Received: by mail.gandi.net (Postfix) with ESMTPSA id 359FF1C0002; Fri, 10 May 2024 07:11:33 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=bootlin.com; s=gm1; t=1715325095; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=sI8WmkUUHY9Zro8RyMVYK+yqLrqdOum7SUJ1HQ/OZVw=; b=S8MGmZjUpe2CwX481FBG08hduF7uyIkuMveONJYOOLVTHn6h+mgL63gD/zfNDOh/ulPrNJ QjXcTH0wLhv67CnH9sQYqNwkDGm1ZJFXlJxWjDUKc+019PVKtdH/vJ6+pIqBQwgxMopRqO uljWnYosVZ+6K4vRpwWQvruErgAmv4wTQaIfralTFZnbvarNOW39D1Cum+mw055ClKcjVe 4d2GqJwpPNDGxX4z6DdYKyNApEbprbbgbbarmspN75ih1dWPtW2kxSVXLum/vxlXeEYazZ MdziLkwmauv5sc4QC9Rq9de8a2r230b4+KF27fXXUUlq3ikUzpn0jkvSeIbXew== From: Luca Ceresoli Date: Fri, 10 May 2024 09:10:38 +0200 Subject: [PATCH v2 2/5] drm/bridge: add bridge notifier to be notified of bridge addition and removal MIME-Version: 1.0 Message-Id: <20240510-hotplug-drm-bridge-v2-2-ec32f2c66d56@bootlin.com> References: <20240510-hotplug-drm-bridge-v2-0-ec32f2c66d56@bootlin.com> In-Reply-To: <20240510-hotplug-drm-bridge-v2-0-ec32f2c66d56@bootlin.com> To: Rob Herring , Krzysztof Kozlowski , Conor Dooley , Andrzej Hajda , Neil Armstrong , Robert Foss , Laurent Pinchart , Jonas Karlman , Jernej Skrabec , Maarten Lankhorst , Maxime Ripard , Thomas Zimmermann , David Airlie , Daniel Vetter , Derek Kiernan , Dragan Cvetic , Arnd Bergmann , Greg Kroah-Hartman , Saravana Kannan Cc: Paul Kocialkowski , =?utf-8?q?Herv=C3=A9_Codina?= , Thomas Petazzoni , devicetree@vger.kernel.org, linux-kernel@vger.kernel.org, dri-devel@lists.freedesktop.org, Paul Kocialkowski , Luca Ceresoli X-Mailer: b4 0.13.0 X-GND-Sasl: luca.ceresoli@bootlin.com X-BeenThere: dri-devel@lists.freedesktop.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: Direct Rendering Infrastructure - Development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: dri-devel-bounces@lists.freedesktop.org Sender: "dri-devel" From: Paul Kocialkowski In preparation for allowing bridges to be added to and removed from a DRM card without destroying the whole card, add a DRM bridge notifier. Notified events are addition and removal to/from the global bridge list. Co-developed-by: Luca Ceresoli Signed-off-by: Luca Ceresoli Signed-off-by: Paul Kocialkowski --- drivers/gpu/drm/drm_bridge.c | 35 +++++++++++++++++++++++++++++++++++ include/drm/drm_bridge.h | 19 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/drivers/gpu/drm/drm_bridge.c b/drivers/gpu/drm/drm_bridge.c index 521a71c61b16..245f7fa4ea22 100644 --- a/drivers/gpu/drm/drm_bridge.c +++ b/drivers/gpu/drm/drm_bridge.c @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -197,6 +198,36 @@ static DEFINE_MUTEX(bridge_lock); static LIST_HEAD(bridge_list); +static BLOCKING_NOTIFIER_HEAD(bridge_notifier); + +/** + * drm_bridge_notifier_register - add a DRM bridge notifier + * @nb: the notifier block to be registered + * + * The notifier block will be notified of events defined in + * &drm_bridge_notifier_event + */ +int drm_bridge_notifier_register(struct notifier_block *nb) +{ + return blocking_notifier_chain_register(&bridge_notifier, nb); +} +EXPORT_SYMBOL(drm_bridge_notifier_register); + +/** + * drm_bridge_notifier_unregister - remove a DRM bridge notifier + * @nb: the notifier block to be unregistered + */ +int drm_bridge_notifier_unregister(struct notifier_block *nb) +{ + return blocking_notifier_chain_unregister(&bridge_notifier, nb); +} +EXPORT_SYMBOL(drm_bridge_notifier_unregister); + +static void drm_bridge_notifier_notify(unsigned long event, + struct drm_bridge *bridge) +{ + blocking_notifier_call_chain(&bridge_notifier, event, bridge); +} /** * drm_bridge_add - add the given bridge to the global bridge list @@ -210,6 +241,8 @@ void drm_bridge_add(struct drm_bridge *bridge) mutex_lock(&bridge_lock); list_add_tail(&bridge->list, &bridge_list); mutex_unlock(&bridge_lock); + + drm_bridge_notifier_notify(DRM_BRIDGE_NOTIFY_ADD, bridge); } EXPORT_SYMBOL(drm_bridge_add); @@ -243,6 +276,8 @@ EXPORT_SYMBOL(devm_drm_bridge_add); */ void drm_bridge_remove(struct drm_bridge *bridge) { + drm_bridge_notifier_notify(DRM_BRIDGE_NOTIFY_REMOVE, bridge); + mutex_lock(&bridge_lock); list_del_init(&bridge->list); mutex_unlock(&bridge_lock); diff --git a/include/drm/drm_bridge.h b/include/drm/drm_bridge.h index 4baca0d9107b..ee48c1eb76ae 100644 --- a/include/drm/drm_bridge.h +++ b/include/drm/drm_bridge.h @@ -43,6 +43,22 @@ struct drm_panel; struct edid; struct i2c_adapter; +/** + * enum drm_bridge_notifier_event - DRM bridge events + */ +enum drm_bridge_notifier_event { + /** + * @DRM_BRIDGE_NOTIFY_ADD: A bridge has just been added to the + * global bridge list. See drm_bridge_add(). + */ + DRM_BRIDGE_NOTIFY_ADD, + /** + * @DRM_BRIDGE_NOTIFY_REMOVE: A bridge is about to be removed from + * the global bridge list. See drm_bridge_remove(). + */ + DRM_BRIDGE_NOTIFY_REMOVE, +}; + /** * enum drm_bridge_attach_flags - Flags for &drm_bridge_funcs.attach */ @@ -781,6 +797,9 @@ drm_priv_to_bridge(struct drm_private_obj *priv) return container_of(priv, struct drm_bridge, base); } +int drm_bridge_notifier_register(struct notifier_block *nb); +int drm_bridge_notifier_unregister(struct notifier_block *nb); + void drm_bridge_add(struct drm_bridge *bridge); int devm_drm_bridge_add(struct device *dev, struct drm_bridge *bridge); void drm_bridge_remove(struct drm_bridge *bridge); From patchwork Fri May 10 07:10:39 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Luca Ceresoli X-Patchwork-Id: 13660966 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from gabe.freedesktop.org (gabe.freedesktop.org [131.252.210.177]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.lore.kernel.org (Postfix) with ESMTPS id 92814C25B4F for ; Fri, 10 May 2024 07:11:42 +0000 (UTC) Received: from gabe.freedesktop.org (localhost [127.0.0.1]) by gabe.freedesktop.org (Postfix) with ESMTP id 7820110E5AC; Fri, 10 May 2024 07:11:39 +0000 (UTC) Authentication-Results: gabe.freedesktop.org; dkim=pass (2048-bit key; unprotected) header.d=bootlin.com header.i=@bootlin.com header.b="U80jYivW"; dkim-atps=neutral Received: from relay5-d.mail.gandi.net (relay5-d.mail.gandi.net [217.70.183.197]) by gabe.freedesktop.org (Postfix) with ESMTPS id 77D9D10E60E for ; Fri, 10 May 2024 07:11:38 +0000 (UTC) Received: by mail.gandi.net (Postfix) with ESMTPSA id 2A45C1C0007; Fri, 10 May 2024 07:11:35 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=bootlin.com; s=gm1; t=1715325097; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=wlFIBlmbfYaR36UaypiDCAopZgGq725DcQ4HyrxaC/o=; b=U80jYivWaz+SXXXRRbPrJrAw9kXYaobh3XPeNGwBUHWqTlyidKJUWCfGHeYOSHk70n9o9e rdNwajnDt1ZLuVvyCtAPFyLi0cuZV1BcPh6/7n0rJy2KkUnM/f2vnKu5Rl8r3CEHJdig8K XnwL/h3+YdTI1gmyJ4VkAdlE1W+sJvLE/hmXI4bF0ToDFb27Rwc/pSEG1vOq0oAERdxMYo 6hou/qNpur9ia60UW3p/QriUIE7upcqF/igXaljcdNUjq0kL+sdax6LpnxcVepnR0gu2Pu fJUFNOsmc0XZikwm5OvruLW9Tih4to10xiv1KTOxB2rrmUVK+8p/Vn6YNhcDvA== From: Luca Ceresoli Date: Fri, 10 May 2024 09:10:39 +0200 Subject: [PATCH v2 3/5] drm/encoder: add drm_encoder_cleanup_from() MIME-Version: 1.0 Message-Id: <20240510-hotplug-drm-bridge-v2-3-ec32f2c66d56@bootlin.com> References: <20240510-hotplug-drm-bridge-v2-0-ec32f2c66d56@bootlin.com> In-Reply-To: <20240510-hotplug-drm-bridge-v2-0-ec32f2c66d56@bootlin.com> To: Rob Herring , Krzysztof Kozlowski , Conor Dooley , Andrzej Hajda , Neil Armstrong , Robert Foss , Laurent Pinchart , Jonas Karlman , Jernej Skrabec , Maarten Lankhorst , Maxime Ripard , Thomas Zimmermann , David Airlie , Daniel Vetter , Derek Kiernan , Dragan Cvetic , Arnd Bergmann , Greg Kroah-Hartman , Saravana Kannan Cc: Paul Kocialkowski , =?utf-8?q?Herv=C3=A9_Codina?= , Thomas Petazzoni , devicetree@vger.kernel.org, linux-kernel@vger.kernel.org, dri-devel@lists.freedesktop.org, Paul Kocialkowski , Luca Ceresoli X-Mailer: b4 0.13.0 X-GND-Sasl: luca.ceresoli@bootlin.com X-BeenThere: dri-devel@lists.freedesktop.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: Direct Rendering Infrastructure - Development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: dri-devel-bounces@lists.freedesktop.org Sender: "dri-devel" Supporting hardware whose final part of the DRM pipeline can be physically removed requires the ability to detach all bridges from a given point to the end of the pipeline. Introduce a variant of drm_encoder_cleanup() for this. Signed-off-by: Luca Ceresoli --- Changed in v2: - fix a typo in a comment --- drivers/gpu/drm/drm_encoder.c | 21 +++++++++++++++++++++ include/drm/drm_encoder.h | 1 + 2 files changed, 22 insertions(+) diff --git a/drivers/gpu/drm/drm_encoder.c b/drivers/gpu/drm/drm_encoder.c index 8f2bc6a28482..472dfbefe296 100644 --- a/drivers/gpu/drm/drm_encoder.c +++ b/drivers/gpu/drm/drm_encoder.c @@ -207,6 +207,27 @@ void drm_encoder_cleanup(struct drm_encoder *encoder) } EXPORT_SYMBOL(drm_encoder_cleanup); +/** + * drm_encoder_cleanup_from - remove a given bridge and all the following + * @encoder: encoder whole list of bridges shall be pruned + * @bridge: first bridge to remove + * + * Removes from an encoder all the bridges starting with a given bridge + * and until the end of the chain. + * + * This should not be used in "normal" DRM pipelines. It is only useful for + * devices whose final part of the DRM chain can be physically removed and + * later reconnected (possibly with different hardware). + */ +void drm_encoder_cleanup_from(struct drm_encoder *encoder, struct drm_bridge *bridge) +{ + struct drm_bridge *next; + + list_for_each_entry_safe_from(bridge, next, &encoder->bridge_chain, chain_node) + drm_bridge_detach(bridge); +} +EXPORT_SYMBOL(drm_encoder_cleanup_from); + static void drmm_encoder_alloc_release(struct drm_device *dev, void *ptr) { struct drm_encoder *encoder = ptr; diff --git a/include/drm/drm_encoder.h b/include/drm/drm_encoder.h index 977a9381c8ba..bafcabb24267 100644 --- a/include/drm/drm_encoder.h +++ b/include/drm/drm_encoder.h @@ -320,6 +320,7 @@ static inline struct drm_encoder *drm_encoder_find(struct drm_device *dev, } void drm_encoder_cleanup(struct drm_encoder *encoder); +void drm_encoder_cleanup_from(struct drm_encoder *encoder, struct drm_bridge *bridge); /** * drm_for_each_encoder_mask - iterate over encoders specified by bitmask From patchwork Fri May 10 07:10:40 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Luca Ceresoli X-Patchwork-Id: 13660967 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from gabe.freedesktop.org (gabe.freedesktop.org [131.252.210.177]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.lore.kernel.org (Postfix) with ESMTPS id 351B8C25B10 for ; Fri, 10 May 2024 07:11:45 +0000 (UTC) Received: from gabe.freedesktop.org (localhost [127.0.0.1]) by gabe.freedesktop.org (Postfix) with ESMTP id 4F2FA10E5A9; Fri, 10 May 2024 07:11:44 +0000 (UTC) Authentication-Results: gabe.freedesktop.org; dkim=pass (2048-bit key; unprotected) header.d=bootlin.com header.i=@bootlin.com header.b="NJxcAq6a"; dkim-atps=neutral Received: from relay5-d.mail.gandi.net (relay5-d.mail.gandi.net [217.70.183.197]) by gabe.freedesktop.org (Postfix) with ESMTPS id A2B2B10E672 for ; Fri, 10 May 2024 07:11:40 +0000 (UTC) Received: by mail.gandi.net (Postfix) with ESMTPSA id 42E5B1C0008; Fri, 10 May 2024 07:11:37 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=bootlin.com; s=gm1; t=1715325099; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=9tQ2I912nJpQUdQ/fzcPYIYXSS509BxvAE7KubLaYHk=; b=NJxcAq6asu4d1TUZRMce2Oo7hzModSm7j+waNZvqo6PGi5tRMF1TvzGvHfcZ6DUjN6OxlL MwYdEvhaCXF4leQJh/77rmEpvHyAJu1mCysV1uzB3pRAcFM8pFrhQe/7vqyZICw42veNwO YRCSGATX70thNwER6CPfXkECM0rmr3Fd1cPrY+vHdvqm8jdyu11HNCBCPidCrYN0vfMW5f CPnsj0NHskhU+c4tUodbdX4UXscnD3D/K+87vLbIoL3KqGPJGzRPF1yPaNKTTE1bOTOlzO BDAx/PPptR4UsLRrUmlfLNJfUkXxl1Sy0SOTWrTR99O6zYpqUJIkye9y9sthaA== From: Luca Ceresoli Date: Fri, 10 May 2024 09:10:40 +0200 Subject: [PATCH v2 4/5] drm/bridge: hotplug-bridge: add driver to support hot-pluggable DSI bridges MIME-Version: 1.0 Message-Id: <20240510-hotplug-drm-bridge-v2-4-ec32f2c66d56@bootlin.com> References: <20240510-hotplug-drm-bridge-v2-0-ec32f2c66d56@bootlin.com> In-Reply-To: <20240510-hotplug-drm-bridge-v2-0-ec32f2c66d56@bootlin.com> To: Rob Herring , Krzysztof Kozlowski , Conor Dooley , Andrzej Hajda , Neil Armstrong , Robert Foss , Laurent Pinchart , Jonas Karlman , Jernej Skrabec , Maarten Lankhorst , Maxime Ripard , Thomas Zimmermann , David Airlie , Daniel Vetter , Derek Kiernan , Dragan Cvetic , Arnd Bergmann , Greg Kroah-Hartman , Saravana Kannan Cc: Paul Kocialkowski , =?utf-8?q?Herv=C3=A9_Codina?= , Thomas Petazzoni , devicetree@vger.kernel.org, linux-kernel@vger.kernel.org, dri-devel@lists.freedesktop.org, Paul Kocialkowski , Luca Ceresoli X-Mailer: b4 0.13.0 X-GND-Sasl: luca.ceresoli@bootlin.com X-BeenThere: dri-devel@lists.freedesktop.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: Direct Rendering Infrastructure - Development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: dri-devel-bounces@lists.freedesktop.org Sender: "dri-devel" This driver implements the point of a DRM pipeline where a connector allows removal of all the following bridges up to the panel. The DRM subsystem currently allows hotplug of the monitor but not preceding components. However there are embedded devices where the "tail" of the DRM pipeline, including one or more bridges, can be physically removed: .------------------------. | DISPLAY CONTROLLER | | .---------. .------. | | | ENCODER |<--| CRTC | | | '---------' '------' | '------|-----------------' | | HOTPLUG V CONNECTOR .---------. .--. .-. .---------. .-------. | 0 to N | | _| _| | | 1 to N | | | | BRIDGES |--DSI-->||_ |_ |--DSI-->| BRIDGES |--LVDS-->| PANEL | | | | | | | | | | | '---------' '--' '-' '---------' '-------' [--- fixed components --] [----------- removable add-on -----------] This driver supports such devices, where the final segment of a MIPI DSI bus, including one or more bridges, can be physically disconnected and reconnected at runtime, possibly with a different model. This implementation supports a MIPI DSI bus only, but it is designed to be as far as possible generic and extendable to other busses that have no native hotplug and model ID discovery. This driver does not provide facilities to add and remove the hot-pluggable components from the kernel: this needs to be done by other means (e.g. device tree overlay runtime insertion and removal). The hotplug-bridge gets notified of hot-plugging by the DRM bridge notifier callbacks after they get added or before they get removed. The hotplug-bridge role is to implement the "hot-pluggable connector" in the bridge chain. In this position, what the hotplug-bridge should ideally do is: * communicate with the previous component (bridge or encoder) so that it believes it always has a connected bridge following it and the DRM card is always present * be notified of the addition and removal of the following bridge and attach/detach to/from it * communicate with the following bridge so that it will attach and detach using the normal procedure (as if the entire pipeline were being created or destroyed, not only the tail) * expose the "add-on connected/disconnected" status via the DRM connector connected/disconnected status, so that users of the DRM pipeline know when they can render output on the display However some aspects make it a bit more complex than that. Most notably: * the next bridge can be probed and removed at any moment and all probing sequences need to be handled * the DSI host/device registration process, which adds to the DRM bridge attach process, makes the initial card registration tricky * the need to register and deregister the following bridges at runtime without tearing down the whole DRM card prevents using the functions that are normally recommended * the automatic mechanism to call the appropriate .get_modes operation (typically provided by the panel bridge) cannot work as the panel can disappear and reappear as a different model, so an ad-hoc lookup is needed The code handling these and other tricky aspects is accurately documented by comments in the code. Co-developed-by: Paul Kocialkowski Signed-off-by: Paul Kocialkowski Signed-off-by: Luca Ceresoli --- Changed in v2: - change to be a platform device instantiated from the connector driver instead of a self-standing OF driver - add missing error handling for devm_drm_bridge_add() - various cleanups and style improvements --- MAINTAINERS | 5 + drivers/gpu/drm/bridge/Kconfig | 15 + drivers/gpu/drm/bridge/Makefile | 1 + drivers/gpu/drm/bridge/hotplug-bridge.c | 577 ++++++++++++++++++++++++++++++++ 4 files changed, 598 insertions(+) diff --git a/MAINTAINERS b/MAINTAINERS index 4955501217eb..672c26372c92 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -6712,6 +6712,11 @@ T: git git://anongit.freedesktop.org/drm/drm-misc F: Documentation/devicetree/bindings/display/panel/himax,hx8394.yaml F: drivers/gpu/drm/panel/panel-himax-hx8394.c +DRM DRIVER FOR HOTPLUG VIDEO CONNECTOR BRIDGE +M: Luca Ceresoli +S: Maintained +F: drivers/gpu/drm/bridge/hotplug-bridge.c + DRM DRIVER FOR HX8357D PANELS S: Orphan T: git git://anongit.freedesktop.org/drm/drm-misc diff --git a/drivers/gpu/drm/bridge/Kconfig b/drivers/gpu/drm/bridge/Kconfig index efd996f6c138..409d090ee94d 100644 --- a/drivers/gpu/drm/bridge/Kconfig +++ b/drivers/gpu/drm/bridge/Kconfig @@ -90,6 +90,21 @@ config DRM_FSL_LDB help Support for i.MX8MP DPI-to-LVDS on-SoC encoder. +config DRM_HOTPLUG_BRIDGE + tristate "Hotplug DRM bridge support" + depends on OF + select DRM_PANEL_BRIDGE + select DRM_MIPI_DSI + select DRM_KMS_HELPER + help + Driver for a DRM bridge representing a physical connector that + splits a DRM pipeline into a fixed part and a physically + removable part. The fixed part includes up to the encoder and + zero or more bridges. The removable part includes any following + bridges up to the connector and panel and can be physically + removed and connected at runtime, possibly with different + components. + config DRM_ITE_IT6505 tristate "ITE IT6505 DisplayPort bridge" depends on OF diff --git a/drivers/gpu/drm/bridge/Makefile b/drivers/gpu/drm/bridge/Makefile index 017b5832733b..278f20729c6c 100644 --- a/drivers/gpu/drm/bridge/Makefile +++ b/drivers/gpu/drm/bridge/Makefile @@ -6,6 +6,7 @@ obj-$(CONFIG_DRM_CHRONTEL_CH7033) += chrontel-ch7033.o obj-$(CONFIG_DRM_CROS_EC_ANX7688) += cros-ec-anx7688.o obj-$(CONFIG_DRM_DISPLAY_CONNECTOR) += display-connector.o obj-$(CONFIG_DRM_FSL_LDB) += fsl-ldb.o +obj-$(CONFIG_DRM_HOTPLUG_BRIDGE) += hotplug-bridge.o obj-$(CONFIG_DRM_ITE_IT6505) += ite-it6505.o obj-$(CONFIG_DRM_LONTIUM_LT8912B) += lontium-lt8912b.o obj-$(CONFIG_DRM_LONTIUM_LT9211) += lontium-lt9211.o diff --git a/drivers/gpu/drm/bridge/hotplug-bridge.c b/drivers/gpu/drm/bridge/hotplug-bridge.c new file mode 100644 index 000000000000..cd663b7fbdbd --- /dev/null +++ b/drivers/gpu/drm/bridge/hotplug-bridge.c @@ -0,0 +1,577 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * A DRM bridge representing the split point between a fixed part of the + * DRM pipeline and a physically removable part. The fixed part includes up + * to the encoder and zero or more bridges. Insertion and removal of the + * "downstream" components happens via device driver probe/removal. + * + * Copyright (C) 2024, GE HealthCare + * + * Authors: + * Luca Ceresoli + * Paul Kocialkowski + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +struct hotplug_bridge { + struct device *dev; + + /* Local bridge */ + struct drm_bridge bridge; + /* Local connector for the bridge chain */ + struct drm_connector *connector; + /* Downstream bridge (next in the chain) */ + struct drm_bridge *next_bridge; + struct mutex next_bridge_mutex; + + struct work_struct hpd_work; + struct notifier_block drm_bridge_nb; + + /* Local DSI host, for the downstream DSI device to attach to */ + struct mipi_dsi_host dsi_host; + /* Local DSI device, attached to the upstream DSI host */ + struct mipi_dsi_device *dsi_dev; + /* Upstream DSI host (the actual DSI controller) */ + struct mipi_dsi_host *prev_dsi_host; +}; + +static struct hotplug_bridge *hotplug_bridge_from_drm_bridge(struct drm_bridge *bridge) +{ + return container_of(bridge, struct hotplug_bridge, bridge); +} + +/* + * Attach the remote bridge to the encoder and to the next bridge in the + * chain, if possible. For this to succeed, we need to know: + * + * - the encoder, which is set at the first drm_bridge_attach() time + * - the next bridge, which is obtained via a notifier whenever the next + * bridge is (re)probed, or at probe time in case it was probed before us + * + * In order to handle different execution sequences, this function can be + * called from multiple places and needs to check all the prerequisites + * every time, and it will act only if both are met. + * + * Must be called with hpb->next_bridge_mutex held. + * + * Returns 0 if the encoder was attached successfully, -ENODEV if any of + * the two prerequisites above is not met (no encoder or no next bridge), + * the error returned by drm_bridge_attach() otherwise. + */ +static int hotplug_bridge_attach_to_encoder_chain(struct hotplug_bridge *hpb) +{ + int ret; + + if (!hpb->next_bridge || !hpb->bridge.encoder) + return -ENODEV; + + ret = drm_bridge_attach(hpb->bridge.encoder, hpb->next_bridge, &hpb->bridge, + DRM_BRIDGE_ATTACH_NO_CONNECTOR); + if (ret) + return dev_err_probe(hpb->dev, ret, "drm_bridge_attach failed\n"); + + dev_dbg(hpb->dev, "attached to encoder chain\n"); + + return 0; +} + +/* + * Stop the video pipeline and detach next_bridge. + * + * Must be called with hpb->next_bridge_mutex held. + */ +static void hotplug_bridge_detach_from_encoder_chain(struct hotplug_bridge *hpb) +{ + WARN_ON_ONCE(!hpb->next_bridge); + + dev_dbg(hpb->dev, "detaching from encoder chain\n"); + + drm_atomic_helper_shutdown(hpb->bridge.dev); + + drm_encoder_cleanup_from(hpb->bridge.encoder, hpb->next_bridge); +} + +static void hotplug_bridge_grab(struct hotplug_bridge *hpb) +{ + struct device *dev = hpb->dev; + struct drm_bridge *bridge; + struct drm_panel *panel; + int err; + + mutex_lock(&hpb->next_bridge_mutex); + + if (hpb->next_bridge) + goto out; + + /* + * This is supposed to be replaced by devm_drm_of_get_bridge(), but + * that is a devm_, and we need to remove the panel bridge also on + * next_bridge disconnect. + */ + err = drm_of_find_panel_or_bridge(dev->of_node, 1, 0, &panel, &bridge); + if (err) + goto out; + + /* Convert the remote panel to a bridge */ + if (panel) + bridge = drm_panel_bridge_add(panel); + + hpb->next_bridge = bridge; + + dev_dbg(dev, "grabbed next bridge (%pOFn)\n", hpb->next_bridge->of_node); + + hpb->bridge.pre_enable_prev_first = hpb->next_bridge->pre_enable_prev_first; + + hotplug_bridge_attach_to_encoder_chain(hpb); + + queue_work(system_wq, &hpb->hpd_work); + +out: + mutex_unlock(&hpb->next_bridge_mutex); +} + +/* + * Detach from the next bridge and remove the panel bridge, either on + * release or when the downstream bridge is being removed. + * + * Can be called in these ways: + * + * - bridge_being_removed is NULL: detach unconditionally + * (this is useful on .remove() to teardown everything) + * - bridge_being_removed == hpb->next_bridge: detach + * (the downstream bridge is being removed) + * - bridge_being_removed != hpb->next_bridge: do nothing + * (the bridge being removed is not the downstream bridge) + * + * In all cases, does nothing when there is no downstream bridge. + */ +static void hotplug_bridge_release(struct hotplug_bridge *hpb, + struct drm_bridge *bridge_being_removed) +{ + mutex_lock(&hpb->next_bridge_mutex); + + if (!hpb->next_bridge) + goto out; + + if (bridge_being_removed && bridge_being_removed != hpb->next_bridge) + goto out; + + dev_dbg(hpb->dev, "releasing next bridge (%pOFn)\n", hpb->next_bridge->of_node); + + hotplug_bridge_detach_from_encoder_chain(hpb); + + /* + * This will check that the bridge actually belongs to panel-bridge + * before doing anything with it, so we can safely always call it. + */ + drm_panel_bridge_remove(hpb->next_bridge); + hpb->next_bridge = NULL; + + queue_work(system_wq, &hpb->hpd_work); + +out: + mutex_unlock(&hpb->next_bridge_mutex); +} + +static int hotplug_bridge_notifier_call(struct notifier_block *nb, + unsigned long event, void *private) +{ + struct hotplug_bridge *hpb = container_of(nb, struct hotplug_bridge, drm_bridge_nb); + struct drm_bridge *bridge = private; + + switch (event) { + case DRM_BRIDGE_NOTIFY_ADD: + hotplug_bridge_grab(hpb); + break; + case DRM_BRIDGE_NOTIFY_REMOVE: + hotplug_bridge_release(hpb, bridge); + break; + } + + return NOTIFY_DONE; +} + +static int hotplug_bridge_attach(struct drm_bridge *bridge, + enum drm_bridge_attach_flags flags) +{ + struct hotplug_bridge *hpb = hotplug_bridge_from_drm_bridge(bridge); + struct device *dev = hpb->dev; + struct drm_connector *connector; + struct drm_encoder *encoder = hpb->bridge.encoder; + int err; + + /* Encoder was not yet provided to our bridge */ + if (!encoder) + return -ENODEV; + + /* Connector was already created */ + if (hpb->connector) + return dev_err_probe(dev, -EBUSY, "connector already created\n"); + + connector = drm_bridge_connector_init(bridge->dev, encoder); + if (IS_ERR(connector)) + return dev_err_probe(dev, PTR_ERR(connector), "failed to initialize connector\n"); + + drm_connector_attach_encoder(connector, encoder); + + hpb->connector = connector; + + drm_connector_register(connector); + + mutex_lock(&hpb->next_bridge_mutex); + err = hotplug_bridge_attach_to_encoder_chain(hpb); + mutex_unlock(&hpb->next_bridge_mutex); + + /* -ENODEV is acceptable, in case next_bridge is not yet known */ + if (err == -ENODEV) + err = 0; + + return err; +} + +static void hotplug_bridge_detach(struct drm_bridge *bridge) +{ + struct hotplug_bridge *hpb = hotplug_bridge_from_drm_bridge(bridge); + + mutex_lock(&hpb->next_bridge_mutex); + hotplug_bridge_detach_from_encoder_chain(hpb); + mutex_unlock(&hpb->next_bridge_mutex); + + if (hpb->connector) { + drm_connector_unregister(hpb->connector); + drm_connector_cleanup(hpb->connector); + hpb->connector = NULL; + } +} + +static enum drm_connector_status hotplug_bridge_detect(struct drm_bridge *bridge) +{ + struct hotplug_bridge *hpb = hotplug_bridge_from_drm_bridge(bridge); + + return hpb->next_bridge ? connector_status_connected : connector_status_disconnected; +} + +static void hotplug_bridge_hpd_work_func(struct work_struct *work) +{ + struct hotplug_bridge *hpd = container_of(work, struct hotplug_bridge, hpd_work); + + if (hpd->bridge.dev) + drm_helper_hpd_irq_event(hpd->bridge.dev); +} + +static int hotplug_bridge_get_modes(struct drm_bridge *bridge, struct drm_connector *connector) +{ + struct hotplug_bridge *hpb = hotplug_bridge_from_drm_bridge(bridge); + struct drm_bridge *br; + int nmodes = 0; + + mutex_lock(&hpb->next_bridge_mutex); + + if (!hpb->next_bridge || !hpb->bridge.encoder) + goto out; + + /* + * In non-removable pipelines, drm_bridge_connector_init() stores + * in the bridge_connector a pointer to the first bridge having + * OP_MODES (typically the panel bridge), so the .get_modes op will + * be automatically be called on that bridge. In our case the + * connector is created once when attaching to the encoder, but the + * panel bridge will appear later and can disappear and change at + * runtime, so we need to look for it every time dynamically. + */ + br = hpb->next_bridge; + list_for_each_entry_from(br, &hpb->bridge.encoder->bridge_chain, chain_node) { + if (br->ops & DRM_BRIDGE_OP_MODES) { + nmodes = br->funcs->get_modes(br, connector); + break; + } + } + +out: + mutex_unlock(&hpb->next_bridge_mutex); + + return nmodes; +} + +static const struct drm_bridge_funcs hotplug_bridge_funcs = { + .attach = hotplug_bridge_attach, + .detach = hotplug_bridge_detach, + .detect = hotplug_bridge_detect, + .get_modes = hotplug_bridge_get_modes, +}; + +static int hotplug_bridge_dsi_detach(struct mipi_dsi_host *host, + struct mipi_dsi_device *device_remote) +{ + struct hotplug_bridge *hpb = dev_get_drvdata(host->dev); + + if (!hpb->dsi_dev) + return -ENODEV; + + mipi_dsi_detach(hpb->dsi_dev); + mipi_dsi_device_unregister(hpb->dsi_dev); + hpb->dsi_dev = NULL; + + return 0; +} + +/* + * Attach the local DSI device to the upstream DSI host, possibly with a + * "null" format. + * + * In "normal" bridges this function should be _only_ used as the .attach + * callback of hotplug_bridge_dsi_ops. But "normal" bridges have their + * downstream DSI device always connected, which we don't. When booting + * without anything connected downstream, our upstream bridge could be not + * even calling drm_bridge_add() until we do attach ourselves as a DSI + * device, preventing the whole DRM card from being instantiated. + * + * In order to always have a DRM card after boot, we do call this same + * function while probing in order to attach as a DSI device to the DSI + * master. However during probe we don't know the bus format yet. It would + * be nice to be able to update the format afterwards when a downstream DSI + * device is attaching to our local host, but there is no callback for + * that. To overcome this limitation, this function can be called in two + * ways: + * + * - during probe, to make the upstream bridge happy, when there is no + * next_dsi_dev yet and thus the lanes/format/etc are unknown + * - as the mipi_dsi_host_ops.attach callback proper, as soon as the + * next_dsi_dev is known + * + * The resulting call sequence is: + * + * 1. hotplug_bridge_dsi_attach() called by hotplug_bridge_probe() with + * next_dsi_dev == NULL: we attach to the host but with a fake format + * so the DRM card can be populated. hpb->dsi_dev becomes non-NULL. + * 2. hotplug_bridge_dsi_attach() called as .attach callback from a + * downstream device when it becomes available: we need to detach in + * order to re-attach with the format of the device. hpb->dsi_dev + * is found non-NULL, then reused so it will be non-NULL again. + * 3. hotplug_bridge_dsi_detach() called as the .detach callback by a + * downstream device: cleans up everything normally. hpb->dsi_dev goes + * from non-NULL to NULL. + * 4. hotplug_bridge_dsi_attach() called by a downstream device: attaches + * normally to the upstream DSI host. hpb->dsi_dev goes from NULL to + * non-NULL. + * + * Steps 3 and 4 are the "normal" attach/detach steps as on "normal" + * bridges. + * + * Steps 1 and 2 happen only the first time, steps 3 and 4 will happen + * every time the downstream bridge disconnects and reconnects. + */ +static int hotplug_bridge_dsi_attach(struct mipi_dsi_host *host, + struct mipi_dsi_device *next_dsi_dev) +{ + struct device *dev = host->dev; + struct hotplug_bridge *hpb = dev_get_drvdata(dev); + struct mipi_dsi_device *dsi_dev; + const struct mipi_dsi_device_info dsi_info = { + .type = "hotplug-bridge", + .channel = 0, + .node = NULL, + }; + int err; + + /* + * Step 2 only (first time we are called for an actual device + * attaching): clean up the fake attach done at step 1 + */ + if (hpb->dsi_dev) + hotplug_bridge_dsi_detach(&hpb->dsi_host, NULL); + + /* Register a local DSI device with the remote DSI host */ + dsi_dev = mipi_dsi_device_register_full(hpb->prev_dsi_host, + &dsi_info); + if (IS_ERR(dsi_dev)) + return PTR_ERR(dsi_dev); + + /* At step 1 we have no downstream device to get the format from */ + if (next_dsi_dev) { + dsi_dev->channel = next_dsi_dev->channel; + dsi_dev->lanes = next_dsi_dev->lanes; + dsi_dev->format = next_dsi_dev->format; + dsi_dev->mode_flags = next_dsi_dev->mode_flags; + } + + /* Attach our local DSI device to the remote DSI host */ + err = mipi_dsi_attach(dsi_dev); + if (err) { + mipi_dsi_device_unregister(dsi_dev); + return dev_err_probe(dev, err, "failed to attach hotplug dsi device to host\n"); + } + + hpb->dsi_dev = dsi_dev; + + return 0; +} + +/* + * Propagate mipi_dsi_device_transfer() to the upstream DSI host. + * + * Reimplements identically the minimal needed part of + * mipi_dsi_device_transfer(), including the -ENOSYS return value. + */ +static ssize_t hotplug_bridge_dsi_transfer(struct mipi_dsi_host *host, + const struct mipi_dsi_msg *msg) +{ + struct hotplug_bridge *hpb = dev_get_drvdata(host->dev); + const struct mipi_dsi_host_ops *ops; + + if (!hpb->dsi_dev) + return -ENODEV; + + ops = hpb->dsi_dev->host->ops; + + if (!ops || !ops->transfer) + return -ENOSYS; + + return ops->transfer(hpb->dsi_dev->host, msg); +} + +static const struct mipi_dsi_host_ops hotplug_bridge_dsi_ops = { + .attach = hotplug_bridge_dsi_attach, + .detach = hotplug_bridge_dsi_detach, + .transfer = hotplug_bridge_dsi_transfer, +}; + +/* + * Find the upstream DSI host and register our downstream-facing DSI host. + */ +static int hotplug_bridge_dsi_setup(struct hotplug_bridge *hpb) +{ + struct device *dev = hpb->dev; + struct device_node *endpoint; + struct device_node *node; + + endpoint = of_graph_get_endpoint_by_regs(dev->of_node, 0, -1); + node = of_graph_get_remote_port_parent(endpoint); + + hpb->prev_dsi_host = of_find_mipi_dsi_host_by_node(node); + + of_node_put(node); + of_node_put(endpoint); + + if (!hpb->prev_dsi_host) + return -EPROBE_DEFER; + + hpb->dsi_host.dev = dev; + hpb->dsi_host.ops = &hotplug_bridge_dsi_ops; + + return mipi_dsi_host_register(&hpb->dsi_host); +} + +static void hotplug_bridge_dsi_cleanup(struct hotplug_bridge *hpb) +{ + mipi_dsi_host_unregister(&hpb->dsi_host); +} + +static int hotplug_bridge_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct hotplug_bridge *hpb; + struct drm_bridge *bridge; + int err; + + hpb = devm_kzalloc(dev, sizeof(*hpb), GFP_KERNEL); + if (!hpb) + return -ENOMEM; + + device_set_node(dev, dev_fwnode(dev->parent)); + + hpb->dev = dev; + + mutex_init(&hpb->next_bridge_mutex); + INIT_WORK(&hpb->hpd_work, hotplug_bridge_hpd_work_func); + + hpb->drm_bridge_nb.notifier_call = hotplug_bridge_notifier_call; + + err = hotplug_bridge_dsi_setup(hpb); + if (err) { + dev_err_probe(dev, err, "failed to setup DSI\n"); + goto err_unset_node; + } + + bridge = &hpb->bridge; + bridge->of_node = dev->of_node; + bridge->funcs = &hotplug_bridge_funcs; + bridge->type = DRM_MODE_CONNECTOR_DSI; + bridge->ops |= DRM_BRIDGE_OP_DETECT | DRM_BRIDGE_OP_HPD | DRM_BRIDGE_OP_MODES; + + platform_set_drvdata(pdev, hpb); + + err = devm_drm_bridge_add(dev, bridge); + if (err) { + dev_err_probe(dev, err, "failed adding bridge\n"); + goto err_dsi_cleanup; + } + + err = hotplug_bridge_dsi_attach(&hpb->dsi_host, NULL); + if (err) { + dev_err_probe(dev, err, "failed first attach to upstream DSI host\n"); + goto err_dsi_cleanup; + } + + /* To be notified when the next bridge appears... */ + drm_bridge_notifier_register(&hpb->drm_bridge_nb); + + /* ...but also check now, in case the next bridge was probed earlier */ + hotplug_bridge_grab(hpb); + + return 0; + +err_dsi_cleanup: + hotplug_bridge_dsi_cleanup(hpb); +err_unset_node: + device_set_node(dev, NULL); + return err; +} + +static void hotplug_bridge_remove(struct platform_device *pdev) +{ + struct hotplug_bridge *hpb = platform_get_drvdata(pdev); + + cancel_work_sync(&hpb->hpd_work); + + drm_bridge_notifier_unregister(&hpb->drm_bridge_nb); + + hotplug_bridge_release(hpb, NULL); + + hotplug_bridge_dsi_cleanup(hpb); + + device_set_node(hpb->dev, NULL); +} + +static const struct platform_device_id hotplug_bridge_platform_ids[] = { + { .name = "hotplug-dsi-bridge" }, + {}, +}; +MODULE_DEVICE_TABLE(platform, hotplug_bridge_platform_ids); + +static struct platform_driver hotplug_bridge_driver = { + .probe = hotplug_bridge_probe, + .remove_new = hotplug_bridge_remove, + .id_table = hotplug_bridge_platform_ids, + .driver = { + .name = "hotplug-drm-bridge", + }, +}; + +module_platform_driver(hotplug_bridge_driver); + +MODULE_AUTHOR("Luca Ceresoli "); +MODULE_AUTHOR("Paul Kocialkowski "); +MODULE_DESCRIPTION("Hotplug DRM Bridge"); +MODULE_LICENSE("GPL"); From patchwork Fri May 10 07:10:41 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Luca Ceresoli X-Patchwork-Id: 13660968 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from gabe.freedesktop.org (gabe.freedesktop.org [131.252.210.177]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.lore.kernel.org (Postfix) with ESMTPS id 26F56C25B4F for ; Fri, 10 May 2024 07:11:47 +0000 (UTC) Received: from gabe.freedesktop.org (localhost [127.0.0.1]) by gabe.freedesktop.org (Postfix) with ESMTP id 5EF6810E5B3; Fri, 10 May 2024 07:11:46 +0000 (UTC) Authentication-Results: gabe.freedesktop.org; dkim=pass (2048-bit key; unprotected) header.d=bootlin.com header.i=@bootlin.com header.b="cKv4aZNM"; dkim-atps=neutral Received: from relay5-d.mail.gandi.net (relay5-d.mail.gandi.net [217.70.183.197]) by gabe.freedesktop.org (Postfix) with ESMTPS id BC8C110E5A9 for ; Fri, 10 May 2024 07:11:42 +0000 (UTC) Received: by mail.gandi.net (Postfix) with ESMTPSA id 6C3741C000A; Fri, 10 May 2024 07:11:39 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=bootlin.com; s=gm1; t=1715325101; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=W7prO++ZWIkCoDMrn6Uhrn59R5uZQF5zUa4NeJy+SNA=; b=cKv4aZNMdXs4VBDyHTtZhBKsaXL4Ww2i8eJQ8V+4PoXipqgV5TWB0ZOHDS9AJZrrp+2h+T yWln1ewmmoR7c+9QLN54qEVORM36FdcVEqxCQZn0F76Xk9rTu9uHVqatuFvP2fQP/z2G2T qBNFk5gsnvtz/tjxX2QlknT/GeE1zjE8UNcwRRXcnVNfNB2G0CbvP8L7egSa1ldRJ99bNe nGREvm/ULTYooJ5+tFSF+MkUSDKhqhxFymyGAIF2Qw4Wpa7Rlvzdxn7OhPSkOJBWt7gBtQ ehgOTLjYzRyHGl9LrX0XkIZc4CuvLrDFP0DFOum5lsjAQk9tipWsrkWZHK9H1w== From: Luca Ceresoli Date: Fri, 10 May 2024 09:10:41 +0200 Subject: [PATCH v2 5/5] misc: add ge-addon-connector driver MIME-Version: 1.0 Message-Id: <20240510-hotplug-drm-bridge-v2-5-ec32f2c66d56@bootlin.com> References: <20240510-hotplug-drm-bridge-v2-0-ec32f2c66d56@bootlin.com> In-Reply-To: <20240510-hotplug-drm-bridge-v2-0-ec32f2c66d56@bootlin.com> To: Rob Herring , Krzysztof Kozlowski , Conor Dooley , Andrzej Hajda , Neil Armstrong , Robert Foss , Laurent Pinchart , Jonas Karlman , Jernej Skrabec , Maarten Lankhorst , Maxime Ripard , Thomas Zimmermann , David Airlie , Daniel Vetter , Derek Kiernan , Dragan Cvetic , Arnd Bergmann , Greg Kroah-Hartman , Saravana Kannan Cc: Paul Kocialkowski , =?utf-8?q?Herv=C3=A9_Codina?= , Thomas Petazzoni , devicetree@vger.kernel.org, linux-kernel@vger.kernel.org, dri-devel@lists.freedesktop.org, Paul Kocialkowski , Luca Ceresoli X-Mailer: b4 0.13.0 X-GND-Sasl: luca.ceresoli@bootlin.com X-BeenThere: dri-devel@lists.freedesktop.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: Direct Rendering Infrastructure - Development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: dri-devel-bounces@lists.freedesktop.org Sender: "dri-devel" Add a driver to support the runtime hot-pluggable add-on connector on the GE SUNH device. This connector allows connecting and disconnecting an add-on to/from the main device to augment its features. Connection and disconnection can happen at runtime at any moment without notice. Different add-on models can be connected, and each has an EEPROM with a model identifier at a fixed address. The add-on hardware is added and removed using device tree overlay loading and unloading. Co-developed-by: Herve Codina Signed-off-by: Herve Codina Signed-off-by: Luca Ceresoli --- This commit is new in v2. --- MAINTAINERS | 1 + drivers/misc/Kconfig | 15 ++ drivers/misc/Makefile | 1 + drivers/misc/ge-sunh-connector.c | 464 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 481 insertions(+) diff --git a/MAINTAINERS b/MAINTAINERS index 672c26372c92..0bdb4fc496b8 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -9905,6 +9905,7 @@ F: drivers/iio/pressure/mprls0025pa* HOTPLUG CONNECTOR FOR GE SUNH ADDONS M: Luca Ceresoli S: Maintained +F: drivers/misc/ge-sunh-connector.c F: Documentation/devicetree/bindings/connector/ge,sunh-addon-connector.yaml HP BIOSCFG DRIVER diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig index 4fb291f0bf7c..99ef2eccbbaa 100644 --- a/drivers/misc/Kconfig +++ b/drivers/misc/Kconfig @@ -574,6 +574,21 @@ config NSM To compile this driver as a module, choose M here. The module will be called nsm. +config GE_SUNH_CONNECTOR + tristate "GE SUNH hotplug add-on connector" + depends on OF + select OF_OVERLAY + select FW_LOADER + select NVMEM + select DRM_HOTPLUG_BRIDGE + help + Driver for the runtime hot-pluggable add-on connector on the GE SUNH + device. This connector allows connecting and disconnecting an add-on + to/from the main device to augment its features. Connection and + disconnection can be done at runtime at any moment without + notice. Different add-on models can be connected, and each has an EEPROM + with a model identifier at a fixed address. + source "drivers/misc/c2port/Kconfig" source "drivers/misc/eeprom/Kconfig" source "drivers/misc/cb710/Kconfig" diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile index ea6ea5bbbc9c..d973de89bd19 100644 --- a/drivers/misc/Makefile +++ b/drivers/misc/Makefile @@ -68,3 +68,4 @@ obj-$(CONFIG_TMR_INJECT) += xilinx_tmr_inject.o obj-$(CONFIG_TPS6594_ESM) += tps6594-esm.o obj-$(CONFIG_TPS6594_PFSM) += tps6594-pfsm.o obj-$(CONFIG_NSM) += nsm.o +obj-$(CONFIG_GE_SUNH_CONNECTOR) += ge-sunh-connector.o diff --git a/drivers/misc/ge-sunh-connector.c b/drivers/misc/ge-sunh-connector.c new file mode 100644 index 000000000000..a40bf4bb56bf --- /dev/null +++ b/drivers/misc/ge-sunh-connector.c @@ -0,0 +1,464 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * GE SUNH hotplug add-on connector + * + * Driver for the runtime hot-pluggable add-on connector on the GE SUNH + * device. Add-on connection is detected via GPIOs (+ a debugfs + * trigger). On connection, a "base" DT overlay is added that describes + * enough to reach the NVMEM cell with the model ID. Based on the ID, an + * add-on-specific overlay is loaded on top to describe everything else. + * + * Copyright (C) 2024, GE HealthCare + * + * Authors: + * Luca Ceresoli + * Herve Codina + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum sunh_conn_overlay_level { + SUNH_CONN_OVERLAY_BASE, + SUNH_CONN_OVERLAY_ADDON, + SUNH_CONN_OVERLAY_N_LEVELS +}; + +#define SUNH_CONN_N_STATUS_GPIOS 2 +static const char * const sunh_conn_status_gpio_name[SUNH_CONN_N_STATUS_GPIOS] = { + "plugged", "powergood" +}; + +struct sunh_conn { + struct device *dev; + struct gpio_desc *reset_gpio; + struct gpio_desc *status_gpio[SUNH_CONN_N_STATUS_GPIOS]; + + bool plugged; + int ovcs_id[SUNH_CONN_OVERLAY_N_LEVELS]; + struct mutex ovl_mutex; // serialize overlay code + struct notifier_block nvmem_nb; + struct work_struct nvmem_notifier_work; + + struct platform_device *hpb_pdev; + struct dentry *debugfs_root; +}; + +static int sunh_conn_insert_overlay(struct sunh_conn *conn, + enum sunh_conn_overlay_level level, + const char *filename) +{ + const struct firmware *fw; + int err; + + err = request_firmware(&fw, filename, conn->dev); + if (err) + return dev_err_probe(conn->dev, err, "Error requesting overlay %s", filename); + + dev_dbg(conn->dev, "insert overlay %d: %s", level, filename); + err = of_overlay_fdt_apply(fw->data, fw->size, &conn->ovcs_id[level], NULL); + if (err) { + int err2; + + dev_err_probe(conn->dev, err, "Failed to apply overlay %s\n", filename); + + /* changeset may be partially applied */ + err2 = of_overlay_remove(&conn->ovcs_id[level]); + if (err2 < 0) + dev_err_probe(conn->dev, err2, + "Failed to remove failed overlay %s\n", filename); + } + + release_firmware(fw); + return err; +} + +static int sunh_conn_load_base_overlay(struct sunh_conn *conn) +{ + int err = 0; + + mutex_lock(&conn->ovl_mutex); + + if (conn->ovcs_id[0] != 0) { + dev_dbg(conn->dev, "base overlay already loaded\n"); + goto out_unlock; + } + + err = sunh_conn_insert_overlay(conn, 0, "imx8mp-sundv1-addon-base.dtbo"); + +out_unlock: + mutex_unlock(&conn->ovl_mutex); + return err; +} + +static int sunh_conn_load_addon_overlay(struct sunh_conn *conn) +{ + u8 addon_id; + const char *filename; + int err; + + mutex_lock(&conn->ovl_mutex); + + if (conn->ovcs_id[0] == 0) { + dev_dbg(conn->dev, "base overlay not loaded\n"); + err = -EINVAL; + goto out_unlock; + } + + if (conn->ovcs_id[1] != 0) { + dev_dbg(conn->dev, "addon overlay already loaded\n"); + err = -EEXIST; + goto out_unlock; + } + + err = nvmem_cell_read_u8(conn->dev, "id", &addon_id); + if (err) + goto out_unlock; + + dev_dbg(conn->dev, "Found add-on ID %d\n", addon_id); + + switch (addon_id) { + case 23: + filename = "imx8mp-sundv1-addon-13.dtbo"; + break; + case 24: + filename = "imx8mp-sundv1-addon-15.dtbo"; + break; + case 25: + filename = "imx8mp-sundv1-addon-18.dtbo"; + break; + default: + dev_warn(conn->dev, "Unknown add-on ID %d\n", addon_id); + err = -ENODEV; + goto out_unlock; + } + + err = sunh_conn_insert_overlay(conn, 1, filename); + +out_unlock: + mutex_unlock(&conn->ovl_mutex); + return err; +} + +static void sunh_conn_unload_overlays(struct sunh_conn *conn) +{ + int level = SUNH_CONN_OVERLAY_N_LEVELS; + int err; + + mutex_lock(&conn->ovl_mutex); + while (level) { + level--; + + if (conn->ovcs_id[level] == 0) + continue; + + dev_dbg(conn->dev, "remove overlay %d (ovcs id %d)", + level, conn->ovcs_id[level]); + + err = of_overlay_remove(&conn->ovcs_id[level]); + if (err) + dev_err_probe(conn->dev, err, "Failed to remove overlay %d\n", level); + } + mutex_unlock(&conn->ovl_mutex); +} + +static void sunh_conn_reset(struct sunh_conn *conn, bool keep_reset) +{ + dev_dbg(conn->dev, "reset\n"); + + gpiod_set_value_cansleep(conn->reset_gpio, 1); + + if (keep_reset) + return; + + mdelay(10); + gpiod_set_value_cansleep(conn->reset_gpio, 0); + mdelay(10); +} + +static int sunh_conn_detach(struct sunh_conn *conn) +{ + /* Cancel any pending NVMEM notification jobs */ + cancel_work_sync(&conn->nvmem_notifier_work); + + /* Unload previouly loaded overlays */ + sunh_conn_unload_overlays(conn); + + /* Set reset signal to have it set on next plug */ + sunh_conn_reset(conn, true); + + dev_info(conn->dev, "detached\n"); + return 0; +} + +static int sunh_conn_attach(struct sunh_conn *conn) +{ + int err; + + /* Reset the plugged board in order to start from a stable state */ + sunh_conn_reset(conn, false); + + err = sunh_conn_load_base_overlay(conn); + if (err) + goto err; + + /* + * -EPROBE_DEFER can be due to NVMEM cell not yet available, so + * don't give up, an NVMEM event could arrive later + */ + err = sunh_conn_load_addon_overlay(conn); + if (err && err != -EPROBE_DEFER) + goto err; + + dev_info(conn->dev, "attached\n"); + return 0; + +err: + sunh_conn_detach(conn); + return err; +} + +static int sunh_conn_handle_event(struct sunh_conn *conn, bool plugged) +{ + int err; + + if (plugged == conn->plugged) + return 0; + + dev_info(conn->dev, "%s\n", plugged ? "connected" : "disconnected"); + + err = (plugged ? + sunh_conn_attach(conn) : + sunh_conn_detach(conn)); + + conn->plugged = plugged; + + return err; +} + +/* + * Return the current status of the connector as reported by the hardware. + * + * Returns: + * - 0 if not connected (any of the existing status GPIOs not asserted) or + * no status GPIOs exist + * - 1 if connected in a stable manner (all status GPIOs are asserted) + * - a negative error code in case reading the GPIOs fail + */ +static int sunh_conn_get_connector_status(struct sunh_conn *conn) +{ + int status = 0; + int i; + + for (i = 0; i < SUNH_CONN_N_STATUS_GPIOS; i++) { + int val; + + if (!conn->status_gpio[i]) + continue; + + val = gpiod_get_value_cansleep(conn->status_gpio[i]); + + if (val < 0) { + dev_err(conn->dev, "Error reading %s GPIO (%d)\n", + sunh_conn_status_gpio_name[i], val); + return val; + } + + if (val == 0) { + dev_dbg(conn->dev, "%s GPIO deasserted\n", + sunh_conn_status_gpio_name[i]); + return 0; + } + + status = 1; + } + + return status; +} + +static irqreturn_t sunh_conn_gpio_irq(int irq, void *data) +{ + struct sunh_conn *conn = data; + int conn_status; + + conn_status = sunh_conn_get_connector_status(conn); + if (conn_status >= 0) + sunh_conn_handle_event(conn, conn_status); + + return IRQ_HANDLED; +} + +static int plugged_read(void *dat, u64 *val) +{ + struct sunh_conn *conn = dat; + + *val = conn->plugged; + + return 0; +} + +static int plugged_write(void *dat, u64 val) +{ + struct sunh_conn *conn = dat; + + if (val > 1) + return -EINVAL; + + return sunh_conn_handle_event(conn, val); +} + +DEFINE_DEBUGFS_ATTRIBUTE(plugged_fops, plugged_read, plugged_write, "%lld\n"); + +static void sunh_conn_nvmem_notifier_work(struct work_struct *work) +{ + struct sunh_conn *conn = container_of(work, struct sunh_conn, nvmem_notifier_work); + + sunh_conn_load_addon_overlay(conn); +} + +static int sunh_conn_nvmem_notifier(struct notifier_block *nb, unsigned long action, void *arg) +{ + struct sunh_conn *conn = container_of(nb, struct sunh_conn, nvmem_nb); + + if (action == NVMEM_CELL_ADD) + queue_work(system_power_efficient_wq, &conn->nvmem_notifier_work); + + return NOTIFY_OK; +} + +static int sunh_conn_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct sunh_conn *conn; + int conn_status; + int err; + int i; + + const struct platform_device_info hpb_info = { + .parent = dev, + .fwnode = dev->fwnode, + .of_node_reused = true, + .name = "hotplug-dsi-bridge", + .id = PLATFORM_DEVID_NONE, + }; + + /* Cannot load overlay from filesystem before rootfs is mounted */ + if (system_state < SYSTEM_RUNNING) + return -EPROBE_DEFER; + + conn = devm_kzalloc(dev, sizeof(*conn), GFP_KERNEL); + if (!conn) + return -ENOMEM; + + platform_set_drvdata(pdev, conn); + conn->dev = dev; + + mutex_init(&conn->ovl_mutex); + INIT_WORK(&conn->nvmem_notifier_work, sunh_conn_nvmem_notifier_work); + + conn->reset_gpio = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_HIGH); + if (IS_ERR(conn->reset_gpio)) + return dev_err_probe(dev, PTR_ERR(conn->reset_gpio), + "Error getting reset GPIO\n"); + + for (i = 0; i < SUNH_CONN_N_STATUS_GPIOS; i++) { + conn->status_gpio[i] = + devm_gpiod_get_optional(dev, sunh_conn_status_gpio_name[i], GPIOD_IN); + if (IS_ERR(conn->status_gpio[i])) + return dev_err_probe(dev, PTR_ERR(conn->status_gpio[i]), + "Error getting %s GPIO\n", + sunh_conn_status_gpio_name[i]); + } + + conn->hpb_pdev = platform_device_register_full(&hpb_info); + if (IS_ERR(conn->hpb_pdev)) { + err = PTR_ERR(conn->hpb_pdev); + return dev_err_probe(dev, err, "Error registering DRM bridge\n"); + } + + conn->nvmem_nb.notifier_call = sunh_conn_nvmem_notifier; + err = nvmem_register_notifier(&conn->nvmem_nb); + if (err) { + dev_err_probe(dev, err, "Error registering NVMEM notifier\n"); + goto err_unregister_drm_bridge; + } + + for (i = 0; i < SUNH_CONN_N_STATUS_GPIOS; i++) { + if (conn->status_gpio[i]) { + err = devm_request_threaded_irq(dev, gpiod_to_irq(conn->status_gpio[i]), + NULL, sunh_conn_gpio_irq, + IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | + IRQF_ONESHOT, + dev_name(dev), conn); + if (err) { + dev_err_probe(dev, err, "Error getting %s GPIO IRQ\n", + sunh_conn_status_gpio_name[i]); + goto err_nvmem_unregister_notifier; + } + } + } + + conn_status = sunh_conn_get_connector_status(conn); + if (conn_status < 0) { + err = conn_status; + goto err_nvmem_unregister_notifier; + } + + /* Ensure initial state is known and overlay loaded if plugged */ + sunh_conn_handle_event(conn, conn_status); + + conn->debugfs_root = debugfs_create_dir(dev_name(dev), NULL); + debugfs_create_file("plugged", 0644, conn->debugfs_root, conn, &plugged_fops); + + return 0; + +err_nvmem_unregister_notifier: + nvmem_unregister_notifier(&conn->nvmem_nb); + cancel_work_sync(&conn->nvmem_notifier_work); +err_unregister_drm_bridge: + platform_device_unregister(conn->hpb_pdev); + return err; +} + +static void sunh_conn_remove(struct platform_device *pdev) +{ + struct sunh_conn *conn = platform_get_drvdata(pdev); + + debugfs_remove(conn->debugfs_root); + sunh_conn_detach(conn); + + nvmem_unregister_notifier(&conn->nvmem_nb); + cancel_work_sync(&conn->nvmem_notifier_work); + + platform_device_unregister(conn->hpb_pdev); +} + +static const struct of_device_id sunh_conn_dt_ids[] = { + { .compatible = "ge,sunh-addon-connector" }, + {} +}; +MODULE_DEVICE_TABLE(of, sunh_conn_dt_ids); + +static struct platform_driver sunh_conn_driver = { + .driver = { + .name = "sunh-addon-connector", + .of_match_table = sunh_conn_dt_ids, + }, + .probe = sunh_conn_probe, + .remove_new = sunh_conn_remove, +}; +module_platform_driver(sunh_conn_driver); + +MODULE_AUTHOR("Luca Ceresoli "); +MODULE_AUTHOR("Herve Codina "); +MODULE_DESCRIPTION("GE SUNH hotplug add-on connector"); +MODULE_LICENSE("GPL");