Rtwo/kernel/motorola/sm8550/drivers/misc/hdmi_input_mux.c
2025-09-30 19:22:48 -05:00

422 lines
10 KiB
C

// 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 <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/platform_device.h>
#include <linux/fs.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/extcon.h>
#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);