| /* |
| * Copyright 2018 Google, Inc |
| * |
| * 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. |
| */ |
| |
| #include <linux/module.h> |
| #include <linux/of.h> |
| #include <linux/delay.h> |
| #include <linux/platform_device.h> |
| #include <linux/pmic-voter.h> |
| #include <linux/power_supply.h> |
| #include <linux/printk.h> |
| #include <linux/thermal.h> |
| #include <linux/time.h> |
| #include <linux/pm_wakeup.h> |
| |
| #define USB_OVERHEAT_MITIGATION_VOTER "USB_OVERHEAT_MITIGATION_VOTER" |
| |
| static int fake_port_temp = -1; |
| module_param_named( |
| fake_port_temp, fake_port_temp, int, 0600 |
| ); |
| |
| static bool enable = true; |
| module_param_named( |
| enable, enable, bool, 0600 |
| ); |
| |
| struct overheat_info { |
| struct device *dev; |
| struct power_supply *usb_psy; |
| struct votable *usb_icl_votable; |
| struct votable *disable_power_role_switch; |
| struct notifier_block psy_nb; |
| struct delayed_work port_overheat_work; |
| struct wakeup_source overheat_ws; |
| |
| bool usb_connected; |
| bool usb_replug; |
| bool overheat_mitigation; |
| bool overheat_work_running; |
| |
| int begin_temp; |
| int clear_temp; |
| int overheat_work_delay_ms; |
| int polling_freq; |
| int check_status; |
| }; |
| |
| #define PSY_GET_PROP(psy, psp) psy_get_prop(psy, psp, #psp) |
| static inline int psy_get_prop(struct power_supply *psy, |
| enum power_supply_property psp, |
| char *prop_name) |
| { |
| union power_supply_propval val; |
| int ret = 0; |
| |
| if (!psy) |
| return -EINVAL; |
| pr_debug("get %s for '%s'...\n", prop_name, psy->desc->name); |
| ret = power_supply_get_property(psy, psp, &val); |
| if (ret < 0) { |
| pr_err("failed to get %s from '%s', ret=%d\n", prop_name, |
| psy->desc->name, ret); |
| return ret; |
| } |
| pr_debug("get %s for '%s' => %d\n", prop_name, psy->desc->name, |
| val.intval); |
| return val.intval; |
| } |
| |
| static inline int get_dts_vars(struct overheat_info *ovh_info) |
| { |
| struct device *dev = ovh_info->dev; |
| struct device_node *node = dev->of_node; |
| int ret; |
| |
| ret = of_property_read_u32(node, "google,begin-mitigation-temp", |
| &ovh_info->begin_temp); |
| if (ret < 0) { |
| dev_err(ovh_info->dev, |
| "cannot read begin-mitigation-temp, ret=%d\n", ret); |
| return ret; |
| } |
| |
| ret = of_property_read_u32(node, "google,end-mitigation-temp", |
| &ovh_info->clear_temp); |
| if (ret < 0) { |
| dev_err(ovh_info->dev, |
| "cannot read end-mitigation-temp, ret=%d\n", ret); |
| return ret; |
| } |
| |
| ret = of_property_read_u32(node, "google,port-overheat-work-interval", |
| &ovh_info->overheat_work_delay_ms); |
| if (ret < 0) { |
| dev_err(ovh_info->dev, |
| "cannot read port-overheat-work-interval, ret=%d\n", |
| ret); |
| return ret; |
| } |
| |
| ret = of_property_read_u32(node, "google,polling-freq", |
| &ovh_info->polling_freq); |
| if (ret < 0) { |
| dev_err(ovh_info->dev, |
| "cannot read polling-freq, ret=%d\n", ret); |
| return ret; |
| } |
| |
| return 0; |
| } |
| |
| static int suspend_usb(struct overheat_info *ovh_info) |
| { |
| int ret; |
| |
| ovh_info->usb_replug = false; |
| |
| /* disable USB */ |
| ret = vote(ovh_info->disable_power_role_switch, |
| USB_OVERHEAT_MITIGATION_VOTER, true, 0); |
| if (ret < 0) { |
| dev_err(ovh_info->dev, |
| "Couldn't vote for disable_power_role_switch ret=%d\n", |
| ret); |
| return ret; |
| } |
| |
| /* suspend charging */ |
| ret = vote(ovh_info->usb_icl_votable, |
| USB_OVERHEAT_MITIGATION_VOTER, true, 0); |
| if (ret < 0) { |
| dev_err(ovh_info->dev, |
| "Couldn't vote for USB ICL ret=%d\n", ret); |
| return ret; |
| } |
| |
| ovh_info->overheat_mitigation = true; |
| return ret; |
| } |
| |
| static int resume_usb(struct overheat_info *ovh_info) |
| { |
| int ret; |
| |
| /* enable charging */ |
| ret = vote(ovh_info->usb_icl_votable, |
| USB_OVERHEAT_MITIGATION_VOTER, false, 0); |
| if (ret < 0) { |
| dev_err(ovh_info->dev, |
| "Couldn't un-vote for USB ICL ret=%d\n", ret); |
| return ret; |
| } |
| |
| /* enable USB */ |
| ret = vote(ovh_info->disable_power_role_switch, |
| USB_OVERHEAT_MITIGATION_VOTER, false, 0); |
| if (ret < 0) { |
| dev_err(ovh_info->dev, |
| "Couldn't un-vote for disable_power_role_switch ret=%d\n", |
| ret); |
| return ret; |
| } |
| |
| ovh_info->overheat_mitigation = false; |
| ovh_info->usb_replug = false; |
| return ret; |
| } |
| |
| /* |
| * Updated usb_connected and usb_replug status in overheat_info struct |
| */ |
| static int update_usb_status(struct overheat_info *ovh_info) |
| { |
| int ret; |
| bool prev_state = ovh_info->usb_connected; |
| int *check_status = &ovh_info->check_status; |
| |
| if (ovh_info->overheat_mitigation) { |
| // Only check USB status every polling_freq instances |
| *check_status = (*check_status + 1) % ovh_info->polling_freq; |
| if (*check_status > 0) |
| return 0; |
| ret = vote(ovh_info->disable_power_role_switch, |
| USB_OVERHEAT_MITIGATION_VOTER, false, 0); |
| if (ret < 0) { |
| dev_err(ovh_info->dev, |
| "Couldn't un-vote for disable_power_role_switch ret=%d\n", |
| ret); |
| return ret; |
| } |
| msleep(200); |
| } |
| |
| dev_dbg(ovh_info->dev, "Updating USB connected status\n"); |
| ret = PSY_GET_PROP(ovh_info->usb_psy, POWER_SUPPLY_PROP_ONLINE); |
| if (ret < 0) |
| return ret; |
| ovh_info->usb_connected = ret; |
| |
| if (ovh_info->overheat_mitigation) { |
| ret = vote(ovh_info->disable_power_role_switch, |
| USB_OVERHEAT_MITIGATION_VOTER, true, 0); |
| if (ret < 0) { |
| dev_err(ovh_info->dev, |
| "Couldn't un-vote for disable_power_role_switch ret=%d\n", |
| ret); |
| return ret; |
| } |
| } |
| |
| if (ovh_info->usb_connected != prev_state) |
| dev_info(ovh_info->dev, |
| "USB is %sconnected", |
| ovh_info->usb_connected ? "" : "dis"); |
| |
| // USB should be disconnected for two cycles before replug is acked |
| if(ovh_info->overheat_mitigation && !ovh_info->usb_connected && |
| !prev_state) |
| ovh_info->usb_replug = true; |
| |
| return 0; |
| } |
| |
| static inline int get_usb_port_temp(struct overheat_info *ovh_info) |
| { |
| int temp; |
| |
| temp = PSY_GET_PROP(ovh_info->usb_psy, POWER_SUPPLY_PROP_TEMP); |
| |
| if (fake_port_temp > 0) |
| temp = fake_port_temp; |
| |
| if (ovh_info->overheat_mitigation || temp >= ovh_info->begin_temp) |
| dev_info(ovh_info->dev, "Overheat triggered: USB port temp is %d\n", |
| temp); |
| return temp; |
| } |
| |
| static int psy_changed(struct notifier_block *nb, unsigned long action, |
| void *data) |
| { |
| struct power_supply *psy = data; |
| struct overheat_info *ovh_info = |
| container_of(nb, struct overheat_info, psy_nb); |
| |
| if ((action != PSY_EVENT_PROP_CHANGED) || (psy == NULL) || |
| (psy->desc == NULL) || (psy->desc->name == NULL)) |
| return NOTIFY_OK; |
| |
| if (action == PSY_EVENT_PROP_CHANGED && |
| !strcmp(psy->desc->name, "usb")) { |
| dev_dbg(ovh_info->dev, "name=usb evt=%lu\n", action); |
| if (!ovh_info->overheat_work_running) |
| schedule_delayed_work(&ovh_info->port_overheat_work, 0); |
| } |
| return NOTIFY_OK; |
| } |
| |
| static void port_overheat_work(struct work_struct *work) |
| { |
| struct overheat_info *ovh_info = |
| container_of(work, struct overheat_info, |
| port_overheat_work.work); |
| int temp = 0, ret = 0; |
| |
| // Take a wake lock to ensure we poll the temp regularly |
| if (!ovh_info->overheat_work_running) |
| __pm_stay_awake(&ovh_info->overheat_ws); |
| ovh_info->overheat_work_running = true; |
| |
| if (enable) { |
| temp = get_usb_port_temp(ovh_info); |
| if (temp < 0) |
| goto rerun; |
| |
| // Check USB connection status if it's safe to do so |
| if (!ovh_info->overheat_mitigation || |
| temp < ovh_info->clear_temp) { |
| ret = update_usb_status(ovh_info); |
| if (ret < 0) |
| goto rerun; |
| } |
| } |
| |
| if (ovh_info->overheat_mitigation && (!enable || |
| (temp < ovh_info->clear_temp && ovh_info->usb_replug))) { |
| dev_err(ovh_info->dev, "Port overheat mitigated\n"); |
| resume_usb(ovh_info); |
| } else if (!ovh_info->overheat_mitigation && |
| enable && temp > ovh_info->begin_temp) { |
| dev_err(ovh_info->dev, "Port overheat triggered\n"); |
| suspend_usb(ovh_info); |
| goto rerun; |
| } |
| |
| if (ovh_info->overheat_mitigation || ovh_info->usb_connected) |
| goto rerun; |
| // Do not run again, USB port isn't overheated or connected to something |
| ovh_info->overheat_work_running = false; |
| __pm_relax(&ovh_info->overheat_ws); |
| return; |
| |
| rerun: |
| schedule_delayed_work( |
| &ovh_info->port_overheat_work, |
| msecs_to_jiffies(ovh_info->overheat_work_delay_ms)); |
| } |
| |
| static int ovh_probe(struct platform_device *pdev) |
| { |
| int ret = 0; |
| struct overheat_info *ovh_info; |
| struct power_supply *usb_psy; |
| struct votable *usb_icl_votable; |
| struct votable *disable_power_role_switch; |
| |
| usb_psy = power_supply_get_by_name("usb"); |
| if (!usb_psy) |
| return -EPROBE_DEFER; |
| |
| usb_icl_votable = find_votable("USB_ICL"); |
| if (usb_icl_votable == NULL) { |
| pr_err("Couldn't find USB_ICL votable\n"); |
| return -EPROBE_DEFER; |
| } |
| |
| disable_power_role_switch = find_votable("DISABLE_POWER_ROLE_SWITCH"); |
| if (disable_power_role_switch == NULL) { |
| pr_err("Couldn't find DISABLE_POWER_ROLE_SWITCH votable\n"); |
| return -EPROBE_DEFER; |
| } |
| |
| ovh_info = devm_kzalloc(&pdev->dev, sizeof(*ovh_info), GFP_KERNEL); |
| if (!ovh_info) |
| return -ENOMEM; |
| |
| ovh_info->dev = &pdev->dev; |
| ovh_info->usb_icl_votable = usb_icl_votable; |
| ovh_info->disable_power_role_switch = disable_power_role_switch; |
| ovh_info->usb_psy = usb_psy; |
| ovh_info->overheat_mitigation = false; |
| ovh_info->usb_replug = false; |
| ovh_info->usb_connected = false; |
| ovh_info->overheat_work_running = false; |
| |
| ret = get_dts_vars(ovh_info); |
| if (ret < 0) |
| return -ENODEV; |
| |
| // initialize votables |
| vote(ovh_info->usb_icl_votable, |
| USB_OVERHEAT_MITIGATION_VOTER, false, 0); |
| vote(ovh_info->disable_power_role_switch, |
| USB_OVERHEAT_MITIGATION_VOTER, false, 0); |
| |
| // register power supply change notifier |
| ovh_info->psy_nb.notifier_call = psy_changed; |
| ret = power_supply_reg_notifier(&ovh_info->psy_nb); |
| if (ret < 0) { |
| dev_err(ovh_info->dev, |
| "Cannot register power supply notifer, ret=%d\n", ret); |
| return ret; |
| } |
| |
| platform_set_drvdata(pdev, ovh_info); |
| wakeup_source_init(&ovh_info->overheat_ws, "overheat_mitigation"); |
| INIT_DELAYED_WORK(&ovh_info->port_overheat_work, port_overheat_work); |
| schedule_delayed_work(&ovh_info->port_overheat_work, 0); |
| |
| return ret; |
| } |
| |
| static int ovh_remove(struct platform_device *pdev) |
| { |
| struct overheat_info *ovh_info = platform_get_drvdata(pdev); |
| if (ovh_info) |
| wakeup_source_trash(&ovh_info->overheat_ws); |
| |
| return 0; |
| } |
| |
| static const struct of_device_id match_table[] = { |
| { |
| .compatible = "google,overheat_mitigation", |
| }, |
| {}, |
| }; |
| |
| static struct platform_driver ovh_driver = { |
| .driver = { |
| .name = "google,overheat_mitigation", |
| .owner = THIS_MODULE, |
| .of_match_table = match_table, |
| }, |
| .probe = ovh_probe, |
| .remove = ovh_remove, |
| }; |
| |
| module_platform_driver(ovh_driver); |
| MODULE_DESCRIPTION("USB port overheat mitigation driver"); |
| MODULE_AUTHOR("Maggie White <maggiewhite@google.com>"); |
| MODULE_LICENSE("GPL v2"); |