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> --- 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 <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 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 <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/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; +}; + +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 <luca.ceresoli@xxxxxxxxxxx>"); +MODULE_AUTHOR("Herve Codina <herve.codina@xxxxxxxxxxx>"); +MODULE_DESCRIPTION("GE SUNH hotplug add-on connector"); +MODULE_LICENSE("GPL"); -- 2.34.1