From patchwork Tue Sep 17 08:53:06 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Luca Ceresoli X-Patchwork-Id: 829484 Received: from relay1-d.mail.gandi.net (relay1-d.mail.gandi.net [217.70.183.193]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 406A016131A; Tue, 17 Sep 2024 08:53:22 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=217.70.183.193 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1726563205; cv=none; b=VQuMXtv+MoGzxvgo3h90yDFWRehBX/x5F00dXUnAtSm48+77YAbupMRJ+M9CaF+isX+wzIoNMPGOI8fb4Ts2YQFXH6jPRCuWVXMWvu/yufL2K2/cD7xPtObP+LGitEh/QnXXbTDJzLoSCa/0UrwtVAJeqrGqQMmd66nGzPX33d0= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1726563205; c=relaxed/simple; bh=LriJQg8ZJHv8lEHoH5JSBXee6yQEV6ryvMKe5RZYvLg=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=rQ1GuedkWhOVN46dbYdjhHTSw+V1Rf28G4huuAeI0csZOm57Z/yXHWH++O0ztr1I5xe6w/1HDLaBvx7keJybMwlSoS9HBbRynPgRj1zxxNuV9PnNeCsPoiNDLXqxQ4R/d9AY3kAokimfZFeZ1mB1zDR7Gj6lR8qhnmyF2f8/8Ww= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=bootlin.com; spf=pass smtp.mailfrom=bootlin.com; dkim=pass (2048-bit key) header.d=bootlin.com header.i=@bootlin.com header.b=BJoY7kvf; arc=none smtp.client-ip=217.70.183.193 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=bootlin.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=bootlin.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=bootlin.com header.i=@bootlin.com header.b="BJoY7kvf" Received: by mail.gandi.net (Postfix) with ESMTPSA id 85DE124000F; Tue, 17 Sep 2024 08:53:19 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=bootlin.com; s=gm1; t=1726563201; 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=pWWypgKRB8PzJi/zpHcCz4egF/NFlS15AogNtOzFzH8=; b=BJoY7kvfpFe1zSibqxQQNoKTYHNxxjT6wRGANCnxMw9l7S7rFpSQxG84DayuEhby1UaiP3 DfiYsGJKo4tWhBKLv9J/4X+/JkhNuI+bUGgzaGj+vj5U4k0WnuGMGliEPvdvcBhVHlbskB zyfDCTWRja3Qei9vXfn0Nng0aUAFesbTZxEETB75ax1YAcpDauVKJPCUkzz7UqRpDRJfEc RBxhRIwAnlVi0wBmJkUoIDj8ohXeOe/+T2a790TJ5dd7plwcEW6A8FwkvnIh9/14SH11B3 qMwb3LuFegweEC6Q4amanAs1bHZf/MmIOrZOSS4Qrcc38lgTeaj6d/GFu4W2NA== From: Luca Ceresoli Date: Tue, 17 Sep 2024 10:53:06 +0200 Subject: [PATCH v4 2/8] drm/bridge: allow bridges to be informed about added and removed bridges Precedence: bulk X-Mailing-List: linux-fbdev@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Message-Id: <20240917-hotplug-drm-bridge-v4-2-bc4dfee61be6@bootlin.com> References: <20240917-hotplug-drm-bridge-v4-0-bc4dfee61be6@bootlin.com> In-Reply-To: <20240917-hotplug-drm-bridge-v4-0-bc4dfee61be6@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 , Wolfram Sang , "Rafael J. Wysocki" , Lee Jones , Daniel Thompson , Jingoo Han , Helge Deller 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, linux-i2c@vger.kernel.org, linux-fbdev@vger.kernel.org, Paul Kocialkowski , Luca Ceresoli X-Mailer: b4 0.14.1 X-GND-Sasl: luca.ceresoli@bootlin.com In preparation for allowing bridges to be added to and removed from a DRM card without destroying the whole card, add a new DRM bridge function called on addition and removal of bridges. Signed-off-by: Luca Ceresoli --- drivers/gpu/drm/drm_bridge.c | 12 ++++++++++++ include/drm/drm_bridge.h | 23 +++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/drivers/gpu/drm/drm_bridge.c b/drivers/gpu/drm/drm_bridge.c index d44f055dbe3e..c98117f1abec 100644 --- a/drivers/gpu/drm/drm_bridge.c +++ b/drivers/gpu/drm/drm_bridge.c @@ -205,8 +205,14 @@ static LIST_HEAD(bridge_list); */ void drm_bridge_add(struct drm_bridge *bridge) { + struct drm_bridge *br, *tmp; + mutex_init(&bridge->hpd_mutex); + list_for_each_entry_safe(br, tmp, &bridge_list, list) + if (br->funcs->bridge_event_notify) + br->funcs->bridge_event_notify(br, DRM_EVENT_BRIDGE_ADD, bridge); + mutex_lock(&bridge_lock); list_add_tail(&bridge->list, &bridge_list); mutex_unlock(&bridge_lock); @@ -243,10 +249,16 @@ EXPORT_SYMBOL(devm_drm_bridge_add); */ void drm_bridge_remove(struct drm_bridge *bridge) { + struct drm_bridge *br, *tmp; + mutex_lock(&bridge_lock); list_del_init(&bridge->list); mutex_unlock(&bridge_lock); + list_for_each_entry_safe(br, tmp, &bridge_list, list) + if (br->funcs->bridge_event_notify) + br->funcs->bridge_event_notify(br, DRM_EVENT_BRIDGE_REMOVE, bridge); + mutex_destroy(&bridge->hpd_mutex); } EXPORT_SYMBOL(drm_bridge_remove); diff --git a/include/drm/drm_bridge.h b/include/drm/drm_bridge.h index 75019d16be64..635cc24bdeba 100644 --- a/include/drm/drm_bridge.h +++ b/include/drm/drm_bridge.h @@ -54,6 +54,11 @@ enum drm_bridge_attach_flags { DRM_BRIDGE_ATTACH_NO_CONNECTOR = BIT(0), }; +enum drm_bridge_event_type { + DRM_EVENT_BRIDGE_ADD, + DRM_EVENT_BRIDGE_REMOVE, +}; + /** * struct drm_bridge_funcs - drm_bridge control functions */ @@ -676,6 +681,24 @@ struct drm_bridge_funcs { enum hdmi_infoframe_type type, const u8 *buffer, size_t len); + /** + * @bridge_event_notify: + * + * Notify that another bridge is being added or removed. + * + * This callback is optional. Bridges implementing it must always + * check whether the event refers to a bridge they actually need to + * interact with. + * + * @bridge: bridge being notified + * @event: event happened (add/remove bridge) + * @new_bridge: the bridge mentioned by the event (i.e. the bridge + * being added or removed) + */ + void (*bridge_event_notify)(struct drm_bridge *bridge, + enum drm_bridge_event_type event, + struct drm_bridge *event_bridge); + /** * @debugfs_init: * From patchwork Tue Sep 17 08:53:08 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Luca Ceresoli X-Patchwork-Id: 829483 Received: from relay1-d.mail.gandi.net (relay1-d.mail.gandi.net [217.70.183.193]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 3B28B170A07; Tue, 17 Sep 2024 08:53:27 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=217.70.183.193 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1726563211; cv=none; b=UOKcskHRgFkGr5AVHAQiYGysKrDYsHhW9NehwpqIIb4Keaz3vUQSPD+IZzkcK2yATaWebbt8NpROvNRcTOmJV+sXX/yjkpk+LCdOQ+MEEvwyVWkGanAn4m/3zw5cDuoHmWPU2rqXovVoGqshWMLHDsFMn23n5B4KnWkpdDqpbDQ= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1726563211; c=relaxed/simple; bh=PyFm35GaijQM9JvobqPEBL5nRU4vRlDILSZydI3EYk0=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=PEkeTQJAM4MkUeEe1tfIK+bssOBqPSU/t0I5V1w3bPk4P/VzfDZvp7v5TSGJwj2Lq8OR26uo2ulJBJk7J+nlqjDeAB+hB7i4fsDmgrFnNj2kYiRJY416bxrv7qakG1ZXEYg5swZVpY9jrVWGuP7CACF3vbEBOJ8JUietrHgFgeE= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=bootlin.com; spf=pass smtp.mailfrom=bootlin.com; dkim=pass (2048-bit key) header.d=bootlin.com header.i=@bootlin.com header.b=N5LrE+Eb; arc=none smtp.client-ip=217.70.183.193 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=bootlin.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=bootlin.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=bootlin.com header.i=@bootlin.com header.b="N5LrE+Eb" Received: by mail.gandi.net (Postfix) with ESMTPSA id 30CD3240010; Tue, 17 Sep 2024 08:53:24 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=bootlin.com; s=gm1; t=1726563206; 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=bQVWmQlecf1VLSgswZKiWzQTOXcrO71RqJbTNz2dTRI=; b=N5LrE+EbzovAXH/wyR30H10jqNPGv8nG2GWhGtnJKPLANrZiy7g+84sgFCfHPHheB8pKdl zp01REyCrPs6v7aPC6uyaMZl6c99axgK/bYxkmEF8+pmGblassOUk9R5hISh4BaGOQLTIy 4BZdvP/SCjIbNScME0lX+DqgJt2yet5Fe9Ue9353AnKQpg5JFzdisBhLlbl/fqLraEYGAZ WfiWmKOquo2CPWQTGGJ6lNntPZ/5htfbV2bmPzFg8Qd1v+eBh0036a23EqvokfqsH5DMKw 2ArGWijEXDcUKqLGF6KEMySYBWZAT7Ns/e+tSvobhddffZ88MYD6ArOFZa9D3w== From: Luca Ceresoli Date: Tue, 17 Sep 2024 10:53:08 +0200 Subject: [PATCH v4 4/8] drm/bridge: hotplug-bridge: add driver to support hot-pluggable DSI bridges Precedence: bulk X-Mailing-List: linux-fbdev@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Message-Id: <20240917-hotplug-drm-bridge-v4-4-bc4dfee61be6@bootlin.com> References: <20240917-hotplug-drm-bridge-v4-0-bc4dfee61be6@bootlin.com> In-Reply-To: <20240917-hotplug-drm-bridge-v4-0-bc4dfee61be6@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 , Wolfram Sang , "Rafael J. Wysocki" , Lee Jones , Daniel Thompson , Jingoo Han , Helge Deller 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, linux-i2c@vger.kernel.org, linux-fbdev@vger.kernel.org, Paul Kocialkowski , Luca Ceresoli X-Mailer: b4 0.14.1 X-GND-Sasl: luca.ceresoli@bootlin.com 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 a device, 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. The add-on supported by this driver has a MIPI DSI bus traversing the hotplug connector and a DSI to LVDS bridge and an LVDS panel on the add-on. Hovever this driver 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 itself add and remove the bridges or panel on the add-on: this needs to be done by other means, e.g. device tree overlay runtime insertion and removal. The hotplug-bridge gets notified by the DRM bridge core after a removable bridge gets added or before it is 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) * instantiate two DRM connectors (similarly to what the DisplayPort MST code does): - a DSI connector representing the video lines of the hotplug connector; the status is always "disconnected" (no panel is ever attached directly to it) - an LSVD connector representing the classic connection to the panel; this gets added/removed whenever the add-on gets connected/disconnected; the status is always "connected" as the panel is always connected to the preceding bridge 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 some of 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. The userspace representation when the add-on is not connected is: # modetest -c | grep -i '^[a-z0-9]' Connectors: id encoder status name size (mm) modes encoders 38 0 disconnected DSI-1 0x0 0 37 And when it is connected a new connector appears: # modetest -c | grep -i '^[a-z0-9]' Connectors: id encoder status name size (mm) modes encoders 38 0 disconnected DSI-1 0x0 0 37 39 0 connected LVDS-1 344x194 1 37 Co-developed-by: Paul Kocialkowski Signed-off-by: Paul Kocialkowski Signed-off-by: Luca Ceresoli --- Changed in v4: - convert from generic notifiers to the new bridge_event_notify bridge func - improved and updated commit message, adding 'modetest' output with/without add-on - select DRM_DISPLAY_HELPER and DRM_BRIDGE_CONNECTOR, required after commit 9da7ec9b19d8 ("drm/bridge-connector: move to DRM_DISPLAY_HELPER module") Changed in v3: - dynamically add/remove the LVDS connector on hot(un)plug - take the firmware node normally via dev->of_node instead of using device_set_node(); this makes code more self-contained and generic - minor rewordings and cleanups 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 | 17 + drivers/gpu/drm/bridge/Makefile | 1 + drivers/gpu/drm/bridge/hotplug-bridge.c | 661 ++++++++++++++++++++++++++++++++ 4 files changed, 684 insertions(+) diff --git a/MAINTAINERS b/MAINTAINERS index b939284d1350..95a05eb75202 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -7010,6 +7010,11 @@ T: git https://gitlab.freedesktop.org/drm/misc/kernel.git 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 https://gitlab.freedesktop.org/drm/misc/kernel.git diff --git a/drivers/gpu/drm/bridge/Kconfig b/drivers/gpu/drm/bridge/Kconfig index 3eb955333c80..95ad3dec9d97 100644 --- a/drivers/gpu/drm/bridge/Kconfig +++ b/drivers/gpu/drm/bridge/Kconfig @@ -90,6 +90,23 @@ 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 + select DRM_DISPLAY_HELPER + select DRM_BRIDGE_CONNECTOR + 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 7df87b582dca..58f59e5a543a 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..e4073a23e919 --- /dev/null +++ b/drivers/gpu/drm/bridge/hotplug-bridge.c @@ -0,0 +1,661 @@ +// 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 + +/* + * Internal hotplug-bridge data. + * + * We have two 'struct drm_connector' here: + * - fixconn represents the DSI video lines on the hotplug connector where + * the removable part attaches. It is thus always instantiated. + * - dynconn represents the LVDS videl lines where the panel is attached. + * It is part of the removable part of the video pipeline and as such is + * added and removed dynamically based on when the downstream devices + * appear and disappear. + */ +struct hotplug_bridge { + struct device *dev; + + /* Local bridge */ + struct drm_bridge bridge; + + /* Always-present connector (where the removal part will connect to) */ + struct drm_connector *fixconn; + + /* Downstream bridge (next in the chain) */ + struct drm_bridge *next_bridge; + struct mutex next_bridge_mutex; + + /* Pointer to the last bridge exposing OP_MODES */ + struct drm_bridge *bridge_modes; + + /* The "tail" connector that gets added/removed at runtime */ + struct drm_connector dynconn; + + /* 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; + + struct work_struct hpd_work; +}; + +static struct hotplug_bridge *hotplug_bridge_from_drm_bridge(struct drm_bridge *bridge) +{ + return container_of(bridge, struct hotplug_bridge, bridge); +} + +/* -------------------------------------------------------------------------- + * dynconn implementation + */ +static struct hotplug_bridge *hotplug_bridge_from_dynconn(struct drm_connector *conn) +{ + return container_of(conn, struct hotplug_bridge, dynconn); +} + +static int hotplug_bridge_dynconn_get_modes(struct drm_connector *connector) +{ + struct hotplug_bridge *hpb = hotplug_bridge_from_dynconn(connector); + + if (hpb->bridge_modes) + return hpb->bridge_modes->funcs->get_modes(hpb->bridge_modes, connector); + + return 0; +} + +static const struct drm_connector_helper_funcs hotplug_bridge_dynconn_connector_helper_funcs = { + .get_modes = hotplug_bridge_dynconn_get_modes, +}; + +static void hotplug_bridge_dynconn_destroy(struct drm_connector *connector) +{ + struct hotplug_bridge *hpb = hotplug_bridge_from_dynconn(connector); + + drm_connector_unregister(&hpb->dynconn); + drm_connector_cleanup(&hpb->dynconn); + hpb->bridge_modes = NULL; +} + +static const struct drm_connector_funcs dynconn_funcs = { + .destroy = hotplug_bridge_dynconn_destroy, + .atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state, + .atomic_destroy_state = drm_atomic_helper_connector_destroy_state, + .fill_modes = drm_helper_probe_single_connector_modes, +}; + +/* + * In non-removable pipelines using a "bridge connector", + * drm_bridge_connector_init() stores in the bridge_connector a pointer to + * the last bridge having OP_MODES (typically the panel bridge), so the + * .get_modes op will automatically be called on that bridge (it also takes + * pointers to other bridges which we don't care about). The "bridge + * connector" is too restrictive for our use case so we cannot use it. But + * we need a pointer to the modes-providing bridge, so we need to replicate + * that bit of its logic. + */ +static void hotplug_bridge_dynconn_find_bridge(struct hotplug_bridge *hpb) +{ + struct drm_bridge *bridge; + + if (WARN_ON(!hpb->next_bridge || !hpb->bridge.encoder)) + return; + + drm_for_each_bridge_in_chain(hpb->bridge.encoder, bridge) + if (bridge->ops & DRM_BRIDGE_OP_MODES) + hpb->bridge_modes = bridge; +} + +static int hotplug_bridge_dynconn_add(struct hotplug_bridge *hpb) +{ + int err; + + err = drm_connector_init(hpb->bridge.dev, &hpb->dynconn, &dynconn_funcs, + DRM_MODE_CONNECTOR_LVDS); + if (err) + return err; + + drm_atomic_helper_connector_reset(&hpb->dynconn); + + drm_connector_helper_add(&hpb->dynconn, + &hotplug_bridge_dynconn_connector_helper_funcs); + + drm_connector_attach_encoder(&hpb->dynconn, hpb->bridge.encoder); + if (err) + goto err_cleanup; + + hotplug_bridge_dynconn_find_bridge(hpb); + + err = drm_connector_register(&hpb->dynconn); + if (err) + goto err_cleanup; + + return 0; + +err_cleanup: + drm_connector_cleanup(&hpb->dynconn); + hpb->bridge_modes = NULL; + return err; +} + +/* ----------------------------------------------------------------------- */ + +/* + * 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_unlock; + + /* + * 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_unlock; + + /* Convert the remote panel to a bridge */ + if (panel) + bridge = drm_panel_bridge_add(panel); + if (IS_ERR(bridge)) + goto out_unlock; + + 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; + + err = hotplug_bridge_attach_to_encoder_chain(hpb); + if (err) + goto err_panel_bridge_remove; + + err = hotplug_bridge_dynconn_add(hpb); + if (err) + goto err_detach_from_encoder_chain; + + queue_work(system_wq, &hpb->hpd_work); + goto out_unlock; + +err_detach_from_encoder_chain: + hotplug_bridge_detach_from_encoder_chain(hpb); +err_panel_bridge_remove: + drm_panel_bridge_remove(hpb->next_bridge); + hpb->next_bridge = NULL; +out_unlock: + 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; + + hotplug_bridge_dynconn_destroy(&hpb->dynconn); + + 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 void hotplug_bridge_bridge_event_notify(struct drm_bridge *bridge, + enum drm_bridge_event_type event, + struct drm_bridge *event_bridge) +{ + + struct hotplug_bridge *hpb = container_of(bridge, struct hotplug_bridge, bridge); + + switch (event) { + case DRM_EVENT_BRIDGE_ADD: + hotplug_bridge_grab(hpb); + break; + case DRM_EVENT_BRIDGE_REMOVE: + hotplug_bridge_release(hpb, event_bridge); + break; + } +} + +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->fixconn) + 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->fixconn = 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->fixconn) { + drm_connector_unregister(hpb->fixconn); + drm_connector_cleanup(hpb->fixconn); + hpb->fixconn = NULL; + } +} + +/* + * The fixed connector is never attached to a panel, so it should always be + * reported as disconnected. + */ +static enum drm_connector_status hotplug_bridge_detect(struct drm_bridge *bridge) +{ + return connector_status_disconnected; +} + +static void hotplug_bridge_hpd_work_func(struct work_struct *work) +{ + struct hotplug_bridge *hpb = container_of(work, struct hotplug_bridge, hpd_work); + + if (hpb->bridge.dev) + drm_helper_hpd_irq_event(hpb->bridge.dev); +} + +static const struct drm_bridge_funcs hotplug_bridge_funcs = { + .attach = hotplug_bridge_attach, + .detach = hotplug_bridge_detach, + .detect = hotplug_bridge_detect, + .bridge_event_notify = hotplug_bridge_bridge_event_notify, +}; + +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; + + hpb->dev = dev; + + mutex_init(&hpb->next_bridge_mutex); + INIT_WORK(&hpb->hpd_work, hotplug_bridge_hpd_work_func); + + err = hotplug_bridge_dsi_setup(hpb); + if (err) + return dev_err_probe(dev, err, "failed to setup DSI\n"); + + 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; + + 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; + } + + /* + * Since devm_drm_bridge_add() we can be notified of any bridges + * appearing, 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); + 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); + + hotplug_bridge_release(hpb, NULL); + + hotplug_bridge_dsi_cleanup(hpb); +} + +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 Tue Sep 17 08:53:10 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Luca Ceresoli X-Patchwork-Id: 829482 Received: from relay1-d.mail.gandi.net (relay1-d.mail.gandi.net [217.70.183.193]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 24E71165EEF; Tue, 17 Sep 2024 08:53:32 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=217.70.183.193 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1726563215; cv=none; b=dHDhoGRS8Qbcw1Ykj9Ip3WYEE8mTc0xK0YyfcedWEeU8nPMokMyNKtp5keEu43yNYW2A8lMKrU970GLEVLPHp+gKLp95rOYDVLHmNSmvlKouHIa15SWEAUX7UB73qzaZGhXHY6Mlr3Jio35KA2SqweMNPskp67PVS4OY815o65I= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1726563215; c=relaxed/simple; bh=z+d3irWh7mStagZJOo4SmN0ZvOkr+sZJxcsZgV6KR3E=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=oamGMGj9KYmCGrulipiK+QYjKf3jZBACHpMq/yXmO/UKMEigy/TMQacAk31xEnr6E9FfTeCmUyOr7a2xk0GMnWYn9eJZs/auNHpIEENTrDlsOuqLgAI7VchixdNLB/3oQp6nlbxxeWnkbjYs3cpxPhyGbzSXdKNFkIySqbHmca8= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=bootlin.com; spf=pass smtp.mailfrom=bootlin.com; dkim=pass (2048-bit key) header.d=bootlin.com header.i=@bootlin.com header.b=Cb8FfdzX; arc=none smtp.client-ip=217.70.183.193 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=bootlin.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=bootlin.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=bootlin.com header.i=@bootlin.com header.b="Cb8FfdzX" Received: by mail.gandi.net (Postfix) with ESMTPSA id 17F8124000F; Tue, 17 Sep 2024 08:53:29 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=bootlin.com; s=gm1; t=1726563211; 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=NbWGpJQEJyMacI+BAl0HLXVEczFckWr8+kUC/shDjy8=; b=Cb8FfdzXhk4g/9rQXsYItNmhlK4goP3zY1+tFtY78i0f8wuTgUyL/2LfWvtmw27SVW6D3P xfCyqEyCt4Qw44LRO2OXqZymPuitdlOVGiCleFKdImwG6DHaFaDDeRSXPMHPJPxOAeOwYV VSvmSh1LxxmRFlgEVjCRtxJfkx3pQ6bi3SJ4kFNHfetqmQcAHdSegwavtEZ0x7w/ULL8Co z4AKVzIgsaoTmqQJ+2z1k6apFc2LNb+iRxwYjV2GQKjY8SdGgrboDaDQVIOIeo3dL6+zbj 1SHNzhIRYYKunkA8kL1T2MHKHzxbQooxn6gdGqSXtCZN2yPLYM8I6eW8qoZW3Q== From: Luca Ceresoli Date: Tue, 17 Sep 2024 10:53:10 +0200 Subject: [PATCH v4 6/8] backlight: led-backlight: add devlink to supplier LEDs Precedence: bulk X-Mailing-List: linux-fbdev@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Message-Id: <20240917-hotplug-drm-bridge-v4-6-bc4dfee61be6@bootlin.com> References: <20240917-hotplug-drm-bridge-v4-0-bc4dfee61be6@bootlin.com> In-Reply-To: <20240917-hotplug-drm-bridge-v4-0-bc4dfee61be6@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 , Wolfram Sang , "Rafael J. Wysocki" , Lee Jones , Daniel Thompson , Jingoo Han , Helge Deller 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, linux-i2c@vger.kernel.org, linux-fbdev@vger.kernel.org, Paul Kocialkowski , Luca Ceresoli X-Mailer: b4 0.14.1 X-GND-Sasl: luca.ceresoli@bootlin.com led-backlight is a consumer of one or multiple LED class devices, but no devlink is created for such supplier-producer relationship. One consequence is that removal ordered is not correctly enforced. Issues happen for example with the following sections in a device tree overlay: // An LED driver chip pca9632@62 { compatible = "nxp,pca9632"; reg = <0x62>; // ... addon_led_pwm: led-pwm@3 { reg = <3>; label = "addon:led:pwm"; }; }; backlight-addon { compatible = "led-backlight"; leds = <&addon_led_pwm>; brightness-levels = <255>; default-brightness-level = <255>; }; On removal of the above overlay, the LED driver can be removed before the backlight device, resulting in: Unable to handle kernel NULL pointer dereference at virtual address 0000000000000010 ... Call trace: led_put+0xe0/0x140 devm_led_release+0x6c/0x98 Fix by adding a devlink between the consuming led-backlight device and the supplying LED device. Signed-off-by: Luca Ceresoli --- This patch first appeared in v4. --- drivers/video/backlight/led_bl.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/drivers/video/backlight/led_bl.c b/drivers/video/backlight/led_bl.c index c7aefcd6e4e3..bfbd80728036 100644 --- a/drivers/video/backlight/led_bl.c +++ b/drivers/video/backlight/led_bl.c @@ -209,6 +209,19 @@ static int led_bl_probe(struct platform_device *pdev) return PTR_ERR(priv->bl_dev); } + for (i = 0; i < priv->nb_leds; i++) { + struct device_link *link; + + link = device_link_add(&pdev->dev, priv->leds[0]->dev->parent, + DL_FLAG_AUTOREMOVE_CONSUMER); + if (!link) { + dev_err(&pdev->dev, "Failed to add devlink (consumer %s, supplier %s)\n", + dev_name(&pdev->dev), dev_name(priv->leds[0]->dev->parent)); + backlight_device_unregister(priv->bl_dev); + return -EINVAL; + } + } + for (i = 0; i < priv->nb_leds; i++) { mutex_lock(&priv->leds[i]->led_access); led_sysfs_disable(priv->leds[i]); From patchwork Tue Sep 17 08:53:12 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Luca Ceresoli X-Patchwork-Id: 829481 Received: from relay1-d.mail.gandi.net (relay1-d.mail.gandi.net [217.70.183.193]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id CFCC8178397; Tue, 17 Sep 2024 08:53:37 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=217.70.183.193 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1726563220; cv=none; b=lJ13sy7w9oZ4SbWwMmQ4qrRPhFSVpU/hhhAixL9e/xyJnDQPlpmH1Fu9Vs0BpgNIauI93zLETx90R/DUMy+msTGn6C1r5nimIQwlsI7xq3V4j3o55yOUZvEZykPBJoMTs0L+8sPiCpAVJx5t3tdwihwapVeIol+tDBRFGvwRT1k= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1726563220; c=relaxed/simple; bh=d46UyFg31sQdtl+sD0JkuMUtCzrEA2Rn4kzIuAmWtPk=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=ElGSkag3JFlgliSBxwW7qVCrHilQBGXf4jdcweywVE1hY7fKWrdcOh9ubTEYydPO65ujVHZUwHk0lvDOBdIShwZvMUr86sm3A0g+bp1b5ZW9sfvsIFBLhA6rNc4F/5DbWtsAMrUK2yFkjTF+gTDNAMrOIRQmg5o3ixYsYVPhsxE= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=bootlin.com; spf=pass smtp.mailfrom=bootlin.com; dkim=pass (2048-bit key) header.d=bootlin.com header.i=@bootlin.com header.b=PBtMUpvs; arc=none smtp.client-ip=217.70.183.193 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=bootlin.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=bootlin.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=bootlin.com header.i=@bootlin.com header.b="PBtMUpvs" Received: by mail.gandi.net (Postfix) with ESMTPSA id EC972240013; Tue, 17 Sep 2024 08:53:33 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=bootlin.com; s=gm1; t=1726563216; 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=HXAI/3ju1VCCwHCAXzm98BOlwNQZ+fS3qOU0jxqqHME=; b=PBtMUpvsQ6u8B6br9fNHriTcfs27bbqmsPcDiOOvE/K0+CWYRz7KkOmv96eLDMjFhye0BJ MCDjrTItCm0F/ZfRddlYvKkPyhQKl+4xrPd+REuiksVuV4kPnSjeBO4evplsR19lgrZ93x TCLy9AKbH6uRdSbIjbtG5p6y95P2awGt0h5p1EJ1q3qPCm2uFFxHGEOf7XqlU7SqD+YPTP ryjavBsToaMvdfQ12aiYGivWaPmNIFOB300pqtuW8GFCT0ST8IBlHs1nU/OyHraG1R05b/ Np1MCqZWxZlXqI2duqti4I5eJsScFndC6D9/zsfCIV8mpF9MC6NSw/+fE/V2Zg== From: Luca Ceresoli Date: Tue, 17 Sep 2024 10:53:12 +0200 Subject: [PATCH v4 8/8] misc: add ge-addon-connector driver Precedence: bulk X-Mailing-List: linux-fbdev@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Message-Id: <20240917-hotplug-drm-bridge-v4-8-bc4dfee61be6@bootlin.com> References: <20240917-hotplug-drm-bridge-v4-0-bc4dfee61be6@bootlin.com> In-Reply-To: <20240917-hotplug-drm-bridge-v4-0-bc4dfee61be6@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 , Wolfram Sang , "Rafael J. Wysocki" , Lee Jones , Daniel Thompson , Jingoo Han , Helge Deller 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, linux-i2c@vger.kernel.org, linux-fbdev@vger.kernel.org, Paul Kocialkowski , Luca Ceresoli X-Mailer: b4 0.14.1 X-GND-Sasl: luca.ceresoli@bootlin.com 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 --- Changed in v4: - rename 'nobus-devices' to 'devices' as per new bindings - remove the 'powergood' GPIO, removed from hardware design - rename "base" and "addon" overlays to "overlay 0" and "overlay 1" as the "addon" name is ambiguous - remove unused enum sunh_conn_overlay_level entries, then move the only remaining enum value to a define Changed in v3: - update to the new overlay representation that now does not include the target node; instead the target node is the connector node itself and is now passed by the connector driver to of_overlay_fdt_apply(), so the overlay is now decoupled from the base device tree - update to explicitely probe devices not reachable by the CPU on any physical bus (which probe as platform devices) which are now inside a 'nobus-devices' subnode of the connector node - change how the DRM bridge is populated to use the new device tree representation, having the video ports inside the 'dsi' node **NOTE** this specific change opens up a question about the .of_node_reused flag: setting it to true might be wrong now as the bridge will be handed the 'dsi' subnode of the connector node; however not setting it to true prevents the hotplug bridge module autoloading due to the alias string changing from "platform:hotplug-dsi-bridge" to "of:NdsiT(null)". - remove dev_info() and uninformative dev_dbg() calls - Kconfig: use 'depends on' instead of 'select' - Kconfig: improve help text and add module name This patch first appeared in v2. --- MAINTAINERS | 1 + drivers/misc/Kconfig | 18 ++ drivers/misc/Makefile | 1 + drivers/misc/ge-sunh-connector.c | 481 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 501 insertions(+) diff --git a/MAINTAINERS b/MAINTAINERS index 95a05eb75202..7dec59888df2 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -10278,6 +10278,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 41c54051347a..e2a5bcce0073 100644 --- a/drivers/misc/Kconfig +++ b/drivers/misc/Kconfig @@ -600,6 +600,24 @@ config MARVELL_CN10K_DPI To compile this driver as a module, choose M here: the module will be called mrvl_cn10k_dpi. +config GE_SUNH_CONNECTOR + tristate "GE SUNH hotplug add-on connector" + depends on OF_OVERLAY + depends on NVMEM + depends on DRM_HOTPLUG_BRIDGE + select FW_LOADER + help + Driver for the runtime hot-pluggable add-on connector on the GE + SUNH device. This connector allows connecting an add-on to the + main device to augment its features, and to later disconnect + it. 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. + + To compile this driver as a module, choose M here. + The module will be called ge-sunh-connector. + 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 c2f990862d2b..69747b048046 100644 --- a/drivers/misc/Makefile +++ b/drivers/misc/Makefile @@ -70,4 +70,5 @@ obj-$(CONFIG_TPS6594_ESM) += tps6594-esm.o obj-$(CONFIG_TPS6594_PFSM) += tps6594-pfsm.o obj-$(CONFIG_NSM) += nsm.o obj-$(CONFIG_MARVELL_CN10K_DPI) += mrvl_cn10k_dpi.o +obj-$(CONFIG_GE_SUNH_CONNECTOR) += ge-sunh-connector.o obj-y += keba/ diff --git a/drivers/misc/ge-sunh-connector.c b/drivers/misc/ge-sunh-connector.c new file mode 100644 index 000000000000..0e6064a9a519 --- /dev/null +++ b/drivers/misc/ge-sunh-connector.c @@ -0,0 +1,481 @@ +// 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 +#include + +#define SUNH_CONN_OVERLAY_N_LEVELS 2 + +struct sunh_conn { + struct device *dev; + struct gpio_desc *reset_gpio; + struct gpio_desc *plugged_gpio; + + 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; +}; + +/* + * Populate all platform devices that are not on any bus. + * + * Populate devices without any I/O access from the CPU, (e.g. fixed + * regulators and gpio regulators). In the normal case of a device tree + * without runtime-loaded overlays these are direct children of the root + * node and as such they are populated as a special case. + * + * Within the hotplug connector they need to be at a deeper level of the + * tree. Moreover they are "segregated" in the "devices" node which allows + * to avoid trying of_platform_default_populate() on other kind of nodes. + * + * No need to depopulate them in this driver: of_platform_notify() will do + * that on overlay removal. + * + * In case a generalized framework for OF_based hotplug connector drivers + * will exist in the future, this function is definitely meant for the + * framework. + */ +static int sunh_conn_populate_nobus_devices(struct sunh_conn *conn) +{ + struct device_node *nobus_devs_dn; + int err; + + nobus_devs_dn = of_get_child_by_name(conn->dev->of_node, "devices"); + if (!nobus_devs_dn) + return 0; + + err = of_platform_default_populate(nobus_devs_dn, NULL, conn->dev); + if (err) + dev_err(conn->dev, "Failed to populate nobus devices\n"); + + of_node_put(nobus_devs_dn); + return err; +} + +static int sunh_conn_insert_overlay(struct sunh_conn *conn, + unsigned int 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], conn->dev->of_node); + if (err) + dev_err_probe(conn->dev, err, "Failed to apply overlay %s\n", filename); + else + err = sunh_conn_populate_nobus_devices(conn); + + if (err) { + int err2; + + /* 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; +} + +/* Load the overlay common to all add-ons (enough to read the model ID) */ +static int sunh_conn_load_overlay_0(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; +} + +/* Load the model-specific overlay describing everything not in the overlay 0 */ +static int sunh_conn_load_overlay_1(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) +{ + 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); + + 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_overlay_0(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_overlay_1(conn); + if (err && err != -EPROBE_DEFER) + goto err; + + 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_dbg(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, + * logging any errors. + */ +static int sunh_conn_get_connector_status(struct sunh_conn *conn) +{ + int val = gpiod_get_value_cansleep(conn->plugged_gpio); + + if (val < 0) + dev_err(conn->dev, "Error reading plugged GPIO (%d)\n", val); + + return val; +} + +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_overlay_1(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_register_drm_bridge(struct sunh_conn *conn) +{ + struct device *dev = conn->dev; + struct device_node *dsi_np; + + dsi_np = of_get_child_by_name(dev->of_node, "dsi"); + if (!dsi_np) + return dev_err_probe(dev, -ENOENT, "dsi node not found"); + + const struct platform_device_info hpb_info = { + .parent = dev, + .fwnode = of_fwnode_handle(dsi_np), + .of_node_reused = true, + .name = "hotplug-dsi-bridge", + .id = PLATFORM_DEVID_NONE, + }; + + conn->hpb_pdev = platform_device_register_full(&hpb_info); + of_node_put(dsi_np); // platform core gets/puts the device node + if (IS_ERR(conn->hpb_pdev)) + return dev_err_probe(dev, PTR_ERR(conn->hpb_pdev), + "Error registering DRM bridge\n"); + + return 0; +} + +static void sunh_conn_unregister_drm_bridge(struct sunh_conn *conn) +{ + platform_device_unregister(conn->hpb_pdev); +} + +static int sunh_conn_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct sunh_conn *conn; + int conn_status; + int err; + + /* 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"); + + conn->plugged_gpio = devm_gpiod_get_optional(dev, "plugged", GPIOD_IN); + if (IS_ERR(conn->plugged_gpio)) + return dev_err_probe(dev, PTR_ERR(conn->plugged_gpio), + "Error getting plugged GPIO\n"); + + err = sunh_conn_register_drm_bridge(conn); + if (err) + return err; + + 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; + } + + if (conn->plugged_gpio) { + err = devm_request_threaded_irq(dev, gpiod_to_irq(conn->plugged_gpio), + 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 plugged GPIO IRQ\n"); + 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: + sunh_conn_unregister_drm_bridge(conn); + 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); + + sunh_conn_unregister_drm_bridge(conn); +} + +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");