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 <herve.codina@xxxxxxxxxxx> Signed-off-by: Herve Codina <herve.codina@xxxxxxxxxxx> Signed-off-by: Luca Ceresoli <luca.ceresoli@xxxxxxxxxxx> --- 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 | 523 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 543 insertions(+) diff --git a/MAINTAINERS b/MAINTAINERS index d571aa049ce1..021dfa961f3f 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -10262,6 +10262,7 @@ F: drivers/iio/pressure/mprls0025pa* HOTPLUG CONNECTOR FOR GE SUNH ADDONS M: Luca Ceresoli <luca.ceresoli@xxxxxxxxxxx> 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 41c3d2821a78..8e4cc81b0ec9 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..50c9e35a60e3 --- /dev/null +++ b/drivers/misc/ge-sunh-connector.c @@ -0,0 +1,523 @@ +// 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 <luca.ceresoli@xxxxxxxxxxx> + * Herve Codina <herve.codina@xxxxxxxxxxx> + */ + +#include <linux/debugfs.h> +#include <linux/delay.h> +#include <linux/firmware.h> +#include <linux/gpio/consumer.h> +#include <linux/interrupt.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/nvmem-consumer.h> +#include <linux/of.h> +#include <linux/of_platform.h> +#include <linux/platform_device.h> +#include <linux/workqueue.h> + +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; +}; + +/* + * 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 "nobus-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, "nobus-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, + 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], 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; +} + +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) +{ + 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_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; + + 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. + * + * 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_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; + int i; + + /* 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]); + } + + 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; + } + + 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: + 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 <luca.ceresoli@xxxxxxxxxxx>"); +MODULE_AUTHOR("Herve Codina <herve.codina@xxxxxxxxxxx>"); +MODULE_DESCRIPTION("GE SUNH hotplug add-on connector"); +MODULE_LICENSE("GPL"); -- 2.34.1