// SPDX-License-Identifier: GPL-2.0-only /* * Copyright (c) 2024, Qualcomm Innovation Center, Inc. All rights reserved. * */ #define pr_fmt(fmt) "%s: " fmt, __func__ #include #include #include #include #include #include #include #include #include #include #define HDMI_INPUT_HDMI_IN 1 #define HDMI_INPUT_TYPEC 0 enum hdmi_input_t { NONE, TYPEC, HDMI_IN, }; struct hdmi_input_mux { struct device *dev; struct platform_device *pdev; int irq; u32 hdmi_detect_gpio; u32 hdmi_select_gpio; enum hdmi_input_t hdmi_src; bool hdmi_detect; bool hdmi_auto_switch; struct extcon_dev *edev; struct notifier_block hdmi_sw_nb; }; static int hdmi_input_mux_parse_dt(struct device *dev, struct hdmi_input_mux *pdata) { struct device_node *np = dev->of_node; u32 auto_switch_en = 0; int ret = 0; struct extcon_dev *edev = NULL; pdata->hdmi_detect_gpio = of_get_named_gpio(np, "hdmi-detect-gpio", 0); if (!gpio_is_valid(pdata->hdmi_detect_gpio)) { dev_err(dev, "hdmi detect gpio not specified\n"); ret = -EINVAL; } pr_debug("hdmi-detect-gpio=%d\n", pdata->hdmi_detect_gpio); pdata->hdmi_select_gpio = of_get_named_gpio(np, "hdmi-select-gpio", 0); if (!gpio_is_valid(pdata->hdmi_select_gpio)) { dev_err(dev, "hdmi select gpio not specified\n"); ret = -EINVAL; } pr_debug("hdmi-select-gpio=%d\n", pdata->hdmi_select_gpio); if (!of_property_read_u32(np, "hdmi-auto-switch", &auto_switch_en)) { pr_info("Auto detect HDMI input = %s\n", (auto_switch_en ? "True" : "False")); pdata->hdmi_auto_switch = auto_switch_en; } else { pr_info("hdmi-auto-switch default sets to true.\n"); pdata->hdmi_auto_switch = true; } if (of_property_read_bool(dev->of_node, "extcon")) { edev = extcon_get_edev_by_phandle(pdata->dev, 0); if (IS_ERR(edev) && PTR_ERR(edev) != -ENODEV) { pr_warn("extcon not exist!"); edev = NULL; } } pdata->edev = edev; return ret; } static int hdmi_input_mux_gpio_configure(struct hdmi_input_mux *pdata, bool on) { int ret = 0; if (on) { ret = gpio_request(pdata->hdmi_detect_gpio, "hdmi-detect-gpio"); if (ret) { pr_err("hdmi detect gpio request failed\n"); goto error; } ret = gpio_direction_input(pdata->hdmi_detect_gpio); if (ret) { pr_err("hdmi detect gpio direction failed\n"); goto hdmi_detect_error; } ret = gpio_request(pdata->hdmi_select_gpio, "hdmi-select-gpio"); if (ret) { pr_err("hdmi select gpio request failed\n"); goto hdmi_detect_error; } /* Default select HDMI_IN port as input. */ ret = gpio_direction_output(pdata->hdmi_select_gpio, HDMI_INPUT_HDMI_IN); if (ret) { pr_err("hdmi select gpio direction failed\n"); goto hdmi_select_error; } pdata->hdmi_src = HDMI_IN; } else { gpio_free(pdata->hdmi_select_gpio); gpio_free(pdata->hdmi_detect_gpio); } return ret; hdmi_select_error: gpio_free(pdata->hdmi_select_gpio); hdmi_detect_error: gpio_free(pdata->hdmi_detect_gpio); error: return ret; } static void hdmi_input_mux_send_uevent(struct hdmi_input_mux *pdata) { char name[32], status[32]; char *envp[5]; char *event_string = "HOTPLUG=1"; scnprintf(name, 32, "name = %s", "HDMI_IN"); scnprintf(status, 32, "status = %s", pdata->hdmi_detect ? "connect" : "disconnect"); pr_debug("[%s]:[%s]\n", name, status); envp[0] = name; envp[1] = status; envp[2] = event_string; envp[3] = NULL; envp[4] = NULL; kobject_uevent_env(&pdata->dev->kobj, KOBJ_CHANGE, envp); } static void hdmi_input_mux_auto_switch(struct hdmi_input_mux *pdata) { if (!pdata) return; pr_debug("HDMI-in %s, auto switch to %s\n", (pdata->hdmi_detect ? "connected" : "disconnected"), (pdata->hdmi_detect ? "hdmi" : "type-c")); if (pdata->hdmi_detect) { gpio_set_value(pdata->hdmi_select_gpio, HDMI_INPUT_HDMI_IN); pdata->hdmi_src = HDMI_IN; } else { gpio_set_value(pdata->hdmi_select_gpio, HDMI_INPUT_TYPEC); pdata->hdmi_src = TYPEC; } } static int hdmi_sw_notifier(struct notifier_block *nb, unsigned long event, void *ptr) { struct extcon_dev *edev = ptr; struct hdmi_input_mux *pdata = container_of(nb, struct hdmi_input_mux, hdmi_sw_nb); if (!edev || !pdata) return NOTIFY_DONE; pr_debug("Type-C input %s\n", (event ? "connected" : "disconnected")); pdata->hdmi_detect = !event; if (pdata->hdmi_auto_switch) hdmi_input_mux_auto_switch(pdata); return NOTIFY_DONE; } static irqreturn_t hdmi_input_mux_irq_thread_handler(int irq, void *dev_id) { struct hdmi_input_mux *pdata = (struct hdmi_input_mux *)dev_id; bool old_status; old_status = pdata->hdmi_detect; if (gpio_get_value(pdata->hdmi_detect_gpio) == 1) pdata->hdmi_detect = true; else pdata->hdmi_detect = false; if (old_status != pdata->hdmi_detect) hdmi_input_mux_send_uevent(pdata); pr_debug("irq hdmi in detect:%s!\n", pdata->hdmi_detect ? "connect" : "disconnect"); if (pdata->hdmi_auto_switch) hdmi_input_mux_auto_switch(pdata); return IRQ_HANDLED; } static ssize_t hdmi_det_show(struct device *dev, struct device_attribute *attr, char *buf) { struct hdmi_input_mux *pdata = dev_get_drvdata(dev); return scnprintf(buf, 3, "%d\n", pdata->hdmi_detect ? 1 : 0); } static ssize_t select_show(struct device *dev, struct device_attribute *attr, char *buf) { struct hdmi_input_mux *pdata = dev_get_drvdata(dev); return scnprintf(buf, 7, "%s", (pdata->hdmi_src == HDMI_IN) ? "hdmi\n" : "typec\n"); } static ssize_t select_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { struct hdmi_input_mux *pdata = dev_get_drvdata(dev); if (!pdata) return -EINVAL; gpio_direction_output(pdata->hdmi_select_gpio, 1); if (strncmp(buf, "typec", 5) == 0) { pdata->hdmi_src = TYPEC; gpio_set_value(pdata->hdmi_select_gpio, HDMI_INPUT_TYPEC); pr_debug("typec!\n"); } else if (strncmp(buf, "hdmi", 4) == 0) { pdata->hdmi_src = HDMI_IN; gpio_set_value(pdata->hdmi_select_gpio, HDMI_INPUT_HDMI_IN); pr_debug("hdmi!\n"); } else pr_debug("%s", buf); return count; } static ssize_t auto_show(struct device *dev, struct device_attribute *attr, char *buf) { struct hdmi_input_mux *pdata = dev_get_drvdata(dev); return scnprintf(buf, 6, "%d\n", pdata->hdmi_auto_switch); } static ssize_t auto_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { int auto_switch_en = 0; struct hdmi_input_mux *pdata = dev_get_drvdata(dev); if (!pdata) return -EINVAL; if (kstrtoint(buf, 0, &auto_switch_en) != 0) return -EINVAL; pdata->hdmi_auto_switch = !!auto_switch_en; /* IRQ handler wilil store HDMI-in connect status * even if auto switch was disabled. * Check connect status when auto switch enabled, * or end user need re-plug again. */ if (pdata->hdmi_auto_switch) hdmi_input_mux_auto_switch(pdata); return count; } static DEVICE_ATTR_RO(hdmi_det); static DEVICE_ATTR_RW(select); static DEVICE_ATTR_RW(auto); static struct attribute *hdmi_input_mux_sysfs_attrs[] = { &dev_attr_hdmi_det.attr, &dev_attr_select.attr, &dev_attr_auto.attr, NULL, }; static struct attribute_group hdmi_input_mux_sysfs_attrs_grp = { .attrs = hdmi_input_mux_sysfs_attrs, }; static int hdmi_input_mux_sysfs_init(struct device *dev) { int rc = 0; if (!dev) { pr_err("%s: Invalid params\n", __func__); return -EINVAL; } rc = sysfs_create_group(&dev->kobj, &hdmi_input_mux_sysfs_attrs_grp); if (rc) pr_err("%s: sysfs group creation failed %d\n", __func__, rc); return rc; } static void hdmi_input_mux_sysfs_remove(struct device *dev) { if (!dev) return; sysfs_remove_group(&dev->kobj, &hdmi_input_mux_sysfs_attrs_grp); } static const struct of_device_id hdmi_input_mux_id[] = { { .compatible = "hdmi_input_mux" }, {}, }; MODULE_DEVICE_TABLE(of, hdmi_input_mux_id); // of = Open Firmware static int hdmi_input_mux_probe(struct platform_device *pdev) { struct hdmi_input_mux *pdata; struct device *dev = &pdev->dev; int ret; pdata = devm_kzalloc(dev, sizeof(struct hdmi_input_mux), GFP_KERNEL); pdata->dev = dev; ret = hdmi_input_mux_parse_dt(&pdev->dev, pdata); if (ret) { pr_err("failed to parse device tree\n"); goto error; } ret = hdmi_input_mux_gpio_configure(pdata, true); if (ret) { pr_err("failed to configure GPIOs\n"); goto error; } dev_set_drvdata(dev, pdata); ret = hdmi_input_mux_sysfs_init(dev); if (ret) { pr_err("sysfs init failed\n"); goto error_sysfs; } if (pdata->edev) { pdata->hdmi_sw_nb.notifier_call = hdmi_sw_notifier; ret = extcon_register_notifier(pdata->edev, EXTCON_MECHANICAL, &pdata->hdmi_sw_nb); if (ret < 0) { pr_err("extcon init failed: %d\n", ret); pdata->edev = NULL; } } if (pdata->edev) { pdata->hdmi_detect = !(extcon_get_state(pdata->edev, EXTCON_MECHANICAL)); } else { /* Extcon not exist, detect hdmi hpd pin to do auto switch. */ pdata->hdmi_detect = !!(gpio_get_value(pdata->hdmi_detect_gpio)); pdata->irq = gpio_to_irq(pdata->hdmi_detect_gpio); ret = request_threaded_irq(pdata->irq, NULL, hdmi_input_mux_irq_thread_handler, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | IRQF_ONESHOT, "hdmi_detect_irq", pdata); if (ret) { pr_err("hdmi_detect_irq failed\n"); goto error_irq; } } if (pdata->hdmi_auto_switch) hdmi_input_mux_auto_switch(pdata); dev_info(dev, "%s done\n", __func__); return 0; error_irq: disable_irq(pdata->irq); free_irq(pdata->irq, pdata); error_sysfs: hdmi_input_mux_gpio_configure(pdata, false); error: devm_kfree(dev, pdata); pr_err("hdmi_input_mux init fail!\n"); return -1; } static int hdmi_input_mux_remove(struct platform_device *pdev) { struct hdmi_input_mux *pdata = dev_get_drvdata(&pdev->dev); int ret = 0; if (!pdata) goto end; hdmi_input_mux_sysfs_remove(&pdev->dev); disable_irq(pdata->irq); free_irq(pdata->irq, pdata); ret = hdmi_input_mux_gpio_configure(pdata, false); devm_kfree(&pdev->dev, pdata); pr_info("hdmi_input_mux remove!\n"); end: return ret; } static struct platform_driver hdmi_input_mux_driver = { .probe = hdmi_input_mux_probe, .remove = hdmi_input_mux_remove, .driver = { .name = "hdmi_input_mux", .owner = THIS_MODULE, .of_match_table = of_match_ptr(hdmi_input_mux_id), }, }; static int __init hdmi_input_mux_init(void) { pr_info("hdmi_input_mux init!\n"); return platform_driver_register(&hdmi_input_mux_driver); } static void __exit hdmi_input_mux_exit(void) { pr_info("hdmi_input_mux exit!\n"); platform_driver_unregister(&hdmi_input_mux_driver); } MODULE_LICENSE("GPL"); module_init(hdmi_input_mux_init); module_exit(hdmi_input_mux_exit);