// SPDX-License-Identifier: GPL-2.0
/*
*
* Copyright (C) 2022 Samsung Electronics
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see .
*
*/
#define pr_fmt(fmt) "[MUIC] vt_muic: " fmt
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#if IS_ENABLED(CONFIG_PDIC_NOTIFIER) && \
IS_ENABLED(CONFIG_USB_TYPEC_MANAGER_NOTIFIER)
#include
#include
#endif /* CONFIG_PDIC_NOTIFIER && CONFIG_USB_TYPEC_MANAGER_NOTIFIER */
#if IS_ENABLED(CONFIG_BATTERY_SAMSUNG)
#include
#include <../../drivers/battery/common/sec_charging_common.h>
#endif /* CONFIG_BATTERY_SAMSUNG */
extern struct muic_platform_data muic_pdata;
struct vt_muic_pdata {
int usb_id_gpio;
int use_psy_nb;
int use_manager_nb;
};
struct vt_muic_struct {
int irq;
int attached_dev;
int batt_cable;
int rprd;
int afc_dev;
int id_ta;
int is_afc_started;
int buck_prev_status;
unsigned long irq_debounce_interval;
struct device *dev;
struct vt_muic_pdata *pdata;
struct muic_platform_data *muic_pdata;
struct vt_muic_ic_data *ic_data;
struct power_supply *ppsy;
struct work_struct vt_muic_work;
struct notifier_block psy_nb;
struct notifier_block manager_nb;
};
static struct vt_muic_struct vt_muic;
static void vt_muic_work_func(struct work_struct *work)
{
int attached_dev = vt_muic.attached_dev;
int new_dev = ATTACHED_DEV_NONE_MUIC;
bool attach_update = false;
pr_info("%s attached_dev(%d) batt_cable(%d) rprd(%d) afc_dev(%d)\n",
__func__, attached_dev, vt_muic.batt_cable,
vt_muic.rprd, vt_muic.afc_dev);
#if IS_ENABLED(CONFIG_BATTERY_SAMSUNG)
/* 1. update battery cable */
switch (vt_muic.batt_cable) {
case SEC_BATTERY_CABLE_USB:
new_dev = ATTACHED_DEV_USB_MUIC;
break;
case SEC_BATTERY_CABLE_USB_CDP:
new_dev = ATTACHED_DEV_CDP_MUIC;
break;
case SEC_BATTERY_CABLE_TA:
new_dev = ATTACHED_DEV_TA_MUIC;
break;
case SEC_BATTERY_CABLE_PREPARE_TA:
new_dev = ATTACHED_DEV_TA_MUIC;
#if IS_ENABLED(CONFIG_AFC_CHARGER)
if (!vt_muic.is_afc_started) {
#if IS_ENABLED(CONFIG_CHARGER_SYV660)
if (vt_muic.ic_data && vt_muic.ic_data->m_ops.afc_dpdm_ctrl)
vt_muic.ic_data->m_ops.afc_dpdm_ctrl(1);
#endif
if (vt_muic.id_ta)
muic_afc_set_voltage(0x9);
vt_muic.is_afc_started = 1;
}
#endif
break;
case SEC_BATTERY_CABLE_TIMEOUT:
case SEC_BATTERY_CABLE_UNKNOWN:
new_dev = ATTACHED_DEV_TIMEOUT_OPEN_MUIC;
break;
case SEC_BATTERY_CABLE_NONE:
#if IS_ENABLED(CONFIG_AFC_CHARGER) && IS_ENABLED(CONFIG_CHARGER_SYV660)
if (vt_muic.ic_data && vt_muic.ic_data->m_ops.afc_dpdm_ctrl)
vt_muic.ic_data->m_ops.afc_dpdm_ctrl(0);
/* fallthrough */
#endif
default:
new_dev = ATTACHED_DEV_NONE_MUIC;
vt_muic.afc_dev = ATTACHED_DEV_NONE_MUIC;
vt_muic.is_afc_started = 0;
muic_afc_request_cause_clear();
break;
}
#endif
/* 2. update type-c rd cable */
if (vt_muic.rprd)
new_dev = ATTACHED_DEV_OTG_MUIC;
/* 3. update afc cable */
if (new_dev == ATTACHED_DEV_TA_MUIC &&
vt_muic.afc_dev != ATTACHED_DEV_NONE_MUIC) {
new_dev = vt_muic.afc_dev;
attach_update = true;
}
if (attached_dev == new_dev) {
pr_info("%s: Duplicated(%d), ignore\n", __func__, new_dev);
return;
}
if (new_dev != ATTACHED_DEV_NONE_MUIC) {
if (!attach_update && attached_dev != ATTACHED_DEV_NONE_MUIC)
muic_notifier_detach_attached_dev(attached_dev);
muic_notifier_attach_attached_dev(new_dev);
} else {
if (attached_dev != ATTACHED_DEV_NONE_MUIC)
muic_notifier_detach_attached_dev(attached_dev);
}
vt_muic.attached_dev = new_dev;
}
#if IS_ENABLED(CONFIG_BATTERY_SAMSUNG)
static int vt_muic_psy_nb_func(struct notifier_block *nb, unsigned long val,
void *v)
{
struct power_supply *ppsy = (struct power_supply *)v;
union power_supply_propval pval;
#if IS_ENABLED(CONFIG_AFC_CHARGER) && IS_ENABLED(CONFIG_CHARGER_SYV660)
int buck_status;
#endif
int ret;
if (val != PSY_EVENT_PROP_CHANGED) {
pr_debug("%s: not changed(%ld)\n", __func__, val);
return NOTIFY_DONE;
}
if (!ppsy || !ppsy->desc || !ppsy->desc->name) {
pr_err("%s: ppsy is NULL\n", __func__);
return NOTIFY_DONE;
}
if (strcmp("bc12", ppsy->desc->name)) {
pr_debug("%s: name is not bc12 (%s)\n", __func__,
ppsy->desc->name);
return NOTIFY_DONE;
}
#if IS_ENABLED(CONFIG_AFC_CHARGER) && IS_ENABLED(CONFIG_CHARGER_SYV660)
ret = power_supply_get_property(ppsy,
POWER_SUPPLY_PROP_STATUS, &pval);
if (ret != 0) {
pr_err("failed to get psy prop, ret=%d\n", ret);
return NOTIFY_DONE;
}
buck_status = pval.intval;
#endif
ret = power_supply_get_property(ppsy,
POWER_SUPPLY_PROP_ONLINE, &pval);
if (ret != 0) {
pr_err("failed to get psy prop, ret=%d\n", ret);
return NOTIFY_DONE;
}
#if IS_ENABLED(CONFIG_AFC_CHARGER) && IS_ENABLED(CONFIG_CHARGER_SYV660)
pr_info("%s: buck_status prev=%d new=%d\n", __func__,
vt_muic.buck_prev_status, buck_status);
if (vt_muic.buck_prev_status == SEC_BAT_CHG_MODE_BUCK_OFF &&
(buck_status == SEC_BAT_CHG_MODE_CHARGING ||
buck_status == SEC_BAT_CHG_MODE_CHARGING_OFF)) {
if (vt_muic.ic_data && vt_muic.ic_data->m_ops.afc_dpdm_ctrl)
vt_muic.ic_data->m_ops.afc_dpdm_ctrl(0);
vt_muic.is_afc_started = 0;
vt_muic.buck_prev_status = buck_status;
return NOTIFY_DONE;
}
vt_muic.buck_prev_status = buck_status;
#endif
pr_info("%s: batt_cable prev=%d new=%d\n", __func__,
vt_muic.batt_cable, pval.intval);
vt_muic.batt_cable = pval.intval;
schedule_work(&vt_muic.vt_muic_work);
return NOTIFY_DONE;
}
#endif /* CONFIG_BATTERY_SAMSUNG */
#if IS_ENABLED(CONFIG_PDIC_NOTIFIER) && \
IS_ENABLED(CONFIG_USB_TYPEC_MANAGER_NOTIFIER)
static void vt_muic_handle_pd_attach(PD_NOTI_ATTACH_TYPEDEF *pnoti)
{
if (pnoti->rprd) {
vt_muic.rprd = pnoti->rprd;
schedule_work(&vt_muic.vt_muic_work);
}
}
static void vt_muic_handle_pd_detach(PD_NOTI_ATTACH_TYPEDEF *pnoti)
{
if (vt_muic.rprd) {
vt_muic.rprd = 0;
schedule_work(&vt_muic.vt_muic_work);
}
vt_muic.id_ta = 0;
}
static int vt_muic_manager_nb_func(struct notifier_block *nb,
unsigned long action, void *data)
{
PD_NOTI_TYPEDEF *pnoti = (PD_NOTI_TYPEDEF *)data;
if (pnoti->dest != PDIC_NOTIFY_DEV_MUIC) {
pr_err("%s: dest is invalid(%d)\n", __func__, pnoti->dest);
return NOTIFY_DONE;
}
pr_info("%s: src:%d dest:%d id:%d sub[%d %d %d]\n", __func__,
pnoti->src, pnoti->dest, pnoti->id,
pnoti->sub1, pnoti->sub2, pnoti->sub3);
switch (pnoti->id) {
case PDIC_NOTIFY_ID_ATTACH:
if (pnoti->sub1)
vt_muic_handle_pd_attach((PD_NOTI_ATTACH_TYPEDEF *)
pnoti);
else
vt_muic_handle_pd_detach((PD_NOTI_ATTACH_TYPEDEF *)
pnoti);
break;
case PDIC_NOTIFY_ID_WATER:
break;
case PDIC_NOTIFY_ID_TA:
if (pnoti->sub1)
vt_muic.id_ta = 1;
break;
default:
break;
}
return NOTIFY_DONE;
}
#endif /* CONFIG_PDIC_NOTIFIER && CONFIG_USB_TYPEC_MANAGER_NOTIFIER */
void vt_muic_set_attached_afc_dev(int attached_afc_dev)
{
pr_info("%s: (%d)->(%d)\n", __func__, vt_muic.afc_dev, attached_afc_dev);
if (vt_muic.afc_dev == attached_afc_dev)
return;
vt_muic.afc_dev = attached_afc_dev;
schedule_work(&vt_muic.vt_muic_work);
}
EXPORT_SYMBOL(vt_muic_set_attached_afc_dev);
int vt_muic_get_attached_dev(void)
{
return vt_muic.attached_dev;
}
EXPORT_SYMBOL(vt_muic_get_attached_dev);
int vt_muic_get_afc_disable(void)
{
int ret = 0;
if (vt_muic.muic_pdata)
ret = vt_muic.muic_pdata->afc_disable;
return ret;
}
EXPORT_SYMBOL(vt_muic_get_afc_disable);
void vt_muic_register_ic_data(struct vt_muic_ic_data *ic_data)
{
vt_muic.ic_data = ic_data;
}
EXPORT_SYMBOL(vt_muic_register_ic_data);
static int vt_muic_afc_set_voltage(int voltage)
{
struct vt_muic_ic_data *ic_data = vt_muic.ic_data;
int ret = 0;
if (ic_data && ic_data->m_ops.afc_set_voltage)
ret = ic_data->m_ops.afc_set_voltage(voltage);
return ret;
}
#ifdef CONFIG_MUIC_COMMON_SYSFS
static int vt_muic_get_attached_dev_cb(void *data)
{
return vt_muic.attached_dev;
}
static int vt_muic_get_adc_cb(void *data)
{
struct vt_muic_ic_data *ic_data = vt_muic.ic_data;
int ret = 0;
if (ic_data && ic_data->m_ops.get_adc)
ret = ic_data->m_ops.get_adc(ic_data->mdata);
return ret;
}
static int vt_muic_get_vbus_value_cb(void *data)
{
struct vt_muic_ic_data *ic_data = vt_muic.ic_data;
int ret = 0;
if (ic_data && ic_data->m_ops.get_vbus_value)
ret = ic_data->m_ops.get_vbus_value(ic_data->mdata);
return ret;
}
static void vt_muic_set_afc_disable_cb(void *data)
{
struct vt_muic_ic_data *ic_data = vt_muic.ic_data;
if (ic_data && ic_data->m_ops.set_afc_disable)
ic_data->m_ops.set_afc_disable(ic_data->mdata);
}
#if IS_ENABLED(CONFIG_HICCUP_CHARGER)
static int vt_muic_set_hiccup_cb(void *data, int en)
{
struct vt_muic_ic_data *ic_data = vt_muic.ic_data;
int ret = 0;
if (ic_data && ic_data->m_ops.set_hiccup_mode)
ret = ic_data->m_ops.set_hiccup_mode(ic_data->mdata, en);
return ret;
}
static int vt_muic_set_hiccup_mode_cb(int en)
{
struct vt_muic_ic_data *ic_data = vt_muic.ic_data;
int ret = 0;
if (ic_data && ic_data->m_ops.set_hiccup_mode)
ret = ic_data->m_ops.set_hiccup_mode(ic_data->mdata, en);
return ret;
}
#endif
static void vt_muic_fill_muic_sysfs_cb(struct muic_platform_data *pdata)
{
pdata->sysfs_cb.get_attached_dev = vt_muic_get_attached_dev_cb;
pdata->sysfs_cb.get_adc = vt_muic_get_adc_cb;
pdata->sysfs_cb.get_vbus_value = vt_muic_get_vbus_value_cb;
pdata->sysfs_cb.set_afc_disable = vt_muic_set_afc_disable_cb;
#if IS_ENABLED(CONFIG_HICCUP_CHARGER)
pdata->sysfs_cb.set_hiccup = vt_muic_set_hiccup_cb;
#endif
}
static void vt_muic_fill_muic_cb(struct muic_platform_data *pdata)
{
#if IS_ENABLED(CONFIG_HICCUP_CHARGER)
pdata->muic_set_hiccup_mode_cb = vt_muic_set_hiccup_mode_cb;
#endif
}
#endif
#if IS_ENABLED(CONFIG_OF)
static struct vt_muic_pdata *vt_muic_get_dt(struct device *dev)
{
struct device_node *node;
struct vt_muic_pdata *pdata;
int ret = 0;
node = dev->of_node;
if (!node) {
ret = -ENODEV;
goto err_out;
}
pdata = devm_kzalloc(dev, sizeof(*pdata), GFP_KERNEL);
if (!pdata) {
ret = -ENOMEM;
goto err_out;
}
pdata->usb_id_gpio = of_get_named_gpio(node, "vt_muic,usb_id", 0);
if (gpio_is_valid(pdata->usb_id_gpio)) {
ret = gpio_request(pdata->usb_id_gpio, "usb_id");
if (ret) {
pr_err("%s: usb_id gpio request fail(%d)\n",
__func__, ret);
goto err_out;
}
} else {
pr_info("%s: gpio isn't used\n", __func__);
}
if (of_property_read_bool(node, "use_battery_notifier_callback"))
pdata->use_psy_nb = 1;
else
pdata->use_psy_nb = 0;
if (of_property_read_bool(node, "use_manager_notifier_callback"))
pdata->use_manager_nb = 1;
else
pdata->use_manager_nb = 0;
pr_info("%s: use_psy_nb(%d) use_manager_nb(%d)\n", __func__,
pdata->use_psy_nb, pdata->use_manager_nb);
return pdata;
err_out:
return ERR_PTR(ret);
}
#endif /* CONFIG_OF */
static irqreturn_t vt_muic_irq_thread(int irq, void *data)
{
pr_info("%s\n", __func__);
if (time_after(jiffies, vt_muic.irq_debounce_interval)) {
vt_muic.irq_debounce_interval = jiffies + msecs_to_jiffies(300);
muic_send_lcd_on_uevent(vt_muic.muic_pdata);
}
return IRQ_HANDLED;
}
static int vt_muic_irq_init(void)
{
struct vt_muic_pdata *pdata = vt_muic.pdata;
int ret = 0;
vt_muic.irq_debounce_interval = 0;
if (!pdata)
goto out;
if (!gpio_is_valid(pdata->usb_id_gpio)) {
pr_info("%s: do not need irq, return\n", __func__);
goto out;
}
if (!IS_ENABLED(CONFIG_SEC_FACTORY))
goto out;
vt_muic.irq = gpio_to_irq(pdata->usb_id_gpio);
ret = request_threaded_irq(vt_muic.irq, NULL, vt_muic_irq_thread,
(IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING | IRQF_ONESHOT),
"vt_muic", &vt_muic);
if (ret) {
pr_err("%s: failed request irq %d\n", __func__, ret);
goto out;
}
ret = enable_irq_wake(vt_muic.irq);
if (ret)
pr_err("%s: failed enable irq %d\n", __func__, ret);
out:
return ret;
}
static void vt_muic_data_init(void)
{
vt_muic.attached_dev = ATTACHED_DEV_NONE_MUIC;
vt_muic.batt_cable = -1;
vt_muic.rprd = 0;
vt_muic.afc_dev = ATTACHED_DEV_NONE_MUIC;
vt_muic.id_ta = 0;
vt_muic.buck_prev_status = -1;
vt_muic.is_afc_started = 0;
}
static int vt_muic_probe(struct platform_device *pdev)
{
struct vt_muic_pdata *pdata = pdev->dev.platform_data;
int ret = 0;
pr_info("%s\n", __func__);
if (!pdata) {
#if IS_ENABLED(CONFIG_OF)
pdata = vt_muic_get_dt(&pdev->dev);
if (IS_ERR(pdata)) {
pr_err("there is no device tree!\n");
ret = -ENODEV;
goto out;
}
#else
ret = -ENODEV;
pr_err("there is no platform data!\n");
goto out;
#endif /* CONFIG_OF */
}
vt_muic.dev = &pdev->dev;
vt_muic.pdata = pdata;
vt_muic_data_init();
ret = vt_muic_irq_init();
if (ret)
goto out;
vt_muic.muic_pdata = &muic_pdata;
muic_pdata.drv_data = &vt_muic;
vt_muic.muic_pdata->muic_afc_set_voltage_cb
= vt_muic_afc_set_voltage;
#ifdef CONFIG_MUIC_COMMON_SYSFS
vt_muic_fill_muic_sysfs_cb(vt_muic.muic_pdata);
vt_muic_fill_muic_cb(vt_muic.muic_pdata);
ret = muic_sysfs_init(vt_muic.muic_pdata);
if (ret) {
pr_err("%s: failed to create sysfs\n", __func__);
goto err_free_irq;
}
#endif
#if IS_MODULE(CONFIG_MUIC_NOTIFIER)
if (vt_muic.muic_pdata->init_gpio_cb)
ret = vt_muic.muic_pdata->init_gpio_cb(get_switch_sel());
#else
if (vt_muic.muic_pdata->init_gpio_cb)
ret = vt_muic.muic_pdata->init_gpio_cb();
#endif
if (vt_muic.muic_pdata && vt_muic.muic_pdata->init_switch_dev_cb)
vt_muic.muic_pdata->init_switch_dev_cb();
if (pdata->use_psy_nb || pdata->use_manager_nb) {
muic_notifier_detach_attached_dev(ATTACHED_DEV_UNKNOWN_MUIC);
INIT_WORK(&vt_muic.vt_muic_work, vt_muic_work_func);
#if IS_ENABLED(CONFIG_BATTERY_SAMSUNG)
if (pdata->use_psy_nb) {
vt_muic.batt_cable = SEC_BATTERY_CABLE_NONE;
vt_muic.psy_nb.notifier_call = vt_muic_psy_nb_func;
vt_muic.psy_nb.priority = 0;
ret = power_supply_reg_notifier(&vt_muic.psy_nb);
if (ret < 0)
pr_err("%s: register power supply notifier fail\n",
__func__);
}
#endif /* CONFIG_BATTERY_SAMSUNG */
#if IS_ENABLED(CONFIG_PDIC_NOTIFIER) && \
IS_ENABLED(CONFIG_USB_TYPEC_MANAGER_NOTIFIER)
if (pdata->use_manager_nb) {
ret = manager_notifier_register(&vt_muic.manager_nb,
vt_muic_manager_nb_func,
MANAGER_NOTIFY_PDIC_MUIC);
if (ret < 0)
pr_err("%s: register manager notifier fail\n",
__func__);
}
#endif /* CONFIG_PDIC_NOTIFIER && CONFIG_USB_TYPEC_MANAGER_NOTIFIER */
}
pr_info("%s: done\n", __func__);
return ret;
#ifdef CONFIG_MUIC_COMMON_SYSFS
err_free_irq:
if (vt_muic.irq) {
disable_irq_wake(vt_muic.irq);
free_irq(vt_muic.irq, &vt_muic);
}
#endif
out:
if (gpio_is_valid(pdata->usb_id_gpio))
gpio_free(pdata->usb_id_gpio);
return ret;
}
static int vt_muic_remove(struct platform_device *pdev)
{
struct vt_muic_pdata *pdata = pdev->dev.platform_data;
if (pdata) {
#if IS_ENABLED(CONFIG_PDIC_NOTIFIER) && \
IS_ENABLED(CONFIG_USB_TYPEC_MANAGER_NOTIFIER)
if (pdata->use_manager_nb)
manager_notifier_unregister(&vt_muic.manager_nb);
#endif /* CONFIG_PDIC_NOTIFIER && CONFIG_USB_TYPEC_MANAGER_NOTIFIER */
#if IS_ENABLED(CONFIG_BATTERY_SAMSUNG)
if (pdata->use_psy_nb)
power_supply_unreg_notifier(&vt_muic.psy_nb);
#endif /* CONFIG_BATTERY_SAMSUNG */
if (vt_muic.muic_pdata && vt_muic.muic_pdata->cleanup_switch_dev_cb)
vt_muic.muic_pdata->cleanup_switch_dev_cb();
if (vt_muic.irq) {
disable_irq_wake(vt_muic.irq);
free_irq(vt_muic.irq, &vt_muic);
}
if (gpio_is_valid(pdata->usb_id_gpio))
gpio_free(pdata->usb_id_gpio);
}
return 0;
}
#if IS_ENABLED(CONFIG_OF)
static const struct of_device_id vt_muic_dt_ids[] = {
{ .compatible = "samsung,vt_muic" },
{ }
};
MODULE_DEVICE_TABLE(of, vt_muic_dt_ids);
#endif /* CONFIG_OF */
static struct platform_driver vt_muic_driver = {
.probe = vt_muic_probe,
.remove = vt_muic_remove,
.driver = {
.name = "vt_muic",
.owner = THIS_MODULE,
#if IS_ENABLED(CONFIG_OF)
.of_match_table = of_match_ptr(vt_muic_dt_ids),
#endif /* CONFIG_OF */
},
};
static int vt_muic_init(void)
{
return platform_driver_register(&vt_muic_driver);
}
static void __exit vt_muic_exit(void)
{
platform_driver_unregister(&vt_muic_driver);
}
device_initcall(vt_muic_init);
module_exit(vt_muic_exit);
MODULE_AUTHOR("Samsung USB Team");
MODULE_DESCRIPTION("Virtual Muic");
MODULE_LICENSE("GPL");