/* SPDX-License-Identifier: GPL-2.0 */ /* * Copyright 2018 Google, LLC * * 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 #include #include #include #include #include #include #include #include #include "gbms_power_supply.h" #include "google_psy.h" #define USB_OVERHEAT_MITIGATION_VOTER "USB_OVERHEAT_MITIGATION_VOTER" enum { USB_NO_LIMIT = 0, USB_OVERHEAT_THROTTLE = 1, USB_MAX_THROTTLE_STATE = USB_OVERHEAT_THROTTLE, }; static bool mitigation_enabled = true; module_param_named( enable, mitigation_enabled, bool, 0600 ); struct overheat_event_stats { int plug_temp; int max_temp; int trip_time; int hysteresis_time; int cleared_time; }; struct overheat_info { struct device *dev; struct power_supply *usb_psy; struct gvotable_election *usb_icl_votable; struct gvotable_election *disable_power_role_switch; struct notifier_block psy_nb; struct delayed_work port_overheat_work; struct wakeup_source *overheat_ws; struct overheat_event_stats stats; struct thermal_cooling_device *cooling_dev; bool usb_connected; bool accessory_connected; bool usb_replug; bool overheat_mitigation; bool overheat_work_running; int temp; int plug_temp; int max_temp; ktime_t plug_time; ktime_t trip_time; ktime_t hysteresis_time; int begin_temp; int clear_temp; int overheat_work_delay_ms; int polling_freq; int check_status; unsigned long throttle_state; }; #define OVH_ATTR(_name) \ static ssize_t _name##_show(struct device *dev, \ struct device_attribute *attr, \ char *buf) \ { \ struct overheat_info *ovh_info = dev_get_drvdata(dev); \ \ return scnprintf(buf, PAGE_SIZE, "%d\n", ovh_info->stats._name);\ } \ static DEVICE_ATTR_RO(_name); OVH_ATTR(max_temp); OVH_ATTR(plug_temp); OVH_ATTR(trip_time); OVH_ATTR(hysteresis_time); OVH_ATTR(cleared_time); static struct attribute *ovh_attr[] = { &dev_attr_max_temp.attr, &dev_attr_plug_temp.attr, &dev_attr_hysteresis_time.attr, &dev_attr_trip_time.attr, &dev_attr_cleared_time.attr, NULL, }; static const struct attribute_group ovh_attr_group = { .attrs = ovh_attr, }; static inline ktime_t get_seconds_since_boot(void) { return div_u64(ktime_to_ns(ktime_get_boottime()), NSEC_PER_SEC); } 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 = gvotable_cast_bool_vote(ovh_info->disable_power_role_switch, USB_OVERHEAT_MITIGATION_VOTER, true); 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 = gvotable_cast_int_vote(ovh_info->usb_icl_votable, USB_OVERHEAT_MITIGATION_VOTER, 0, true); if (ret < 0) { dev_err(ovh_info->dev, "Couldn't vote for USB ICL ret=%d\n", ret); return ret; } ovh_info->trip_time = get_seconds_since_boot(); ovh_info->overheat_mitigation = true; return ret; } static int resume_usb(struct overheat_info *ovh_info) { int ret; /* Fill out stats so userspace can read them. */ ovh_info->stats.max_temp = ovh_info->max_temp; ovh_info->stats.plug_temp = ovh_info->plug_temp; ovh_info->stats.trip_time = (int) (ovh_info->trip_time - ovh_info->plug_time); ovh_info->stats.hysteresis_time = (int) (ovh_info->hysteresis_time - ovh_info->trip_time); ovh_info->stats.cleared_time = (int) (get_seconds_since_boot() - ovh_info->hysteresis_time); /* enable charging */ ret = gvotable_cast_int_vote(ovh_info->usb_icl_votable, USB_OVERHEAT_MITIGATION_VOTER, 0, false); if (ret < 0) { dev_err(ovh_info->dev, "Couldn't un-vote for USB ICL ret=%d\n", ret); return ret; } /* enable USB */ ret = gvotable_cast_bool_vote(ovh_info->disable_power_role_switch, USB_OVERHEAT_MITIGATION_VOTER, false); if (ret < 0) { dev_err(ovh_info->dev, "Couldn't un-vote for disable_power_role_switch ret=%d\n", ret); return ret; } /* Notify userspace to read the stats. */ kobject_uevent(&ovh_info->dev->kobj, KOBJ_CHANGE); ovh_info->max_temp = INT_MIN; ovh_info->plug_temp = INT_MIN; ovh_info->plug_time = 0; ovh_info->trip_time = 0; ovh_info->hysteresis_time = 0; ovh_info->overheat_mitigation = false; ovh_info->usb_replug = false; return ret; } /* * Update usb_connected, accessory_connected, usb_replug, and plug_temp * status in overheat_info struct. */ static int update_usb_status(struct overheat_info *ovh_info) { int ret; bool prev_state = ovh_info->usb_connected || ovh_info->accessory_connected; bool curr_state; int *check_status = &ovh_info->check_status; int mode; /* Port is too hot to safely check the connected status. */ if (ovh_info->overheat_mitigation && ovh_info->temp > ovh_info->clear_temp) return -EBUSY; if (ovh_info->overheat_mitigation) { if (!ovh_info->hysteresis_time) ovh_info->hysteresis_time = get_seconds_since_boot(); // Only check USB status every polling_freq instances *check_status = (*check_status + 1) % ovh_info->polling_freq; if (*check_status > 0) return 0; ret = gvotable_cast_bool_vote(ovh_info->disable_power_role_switch, USB_OVERHEAT_MITIGATION_VOTER, false); 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"); /* * Update USB present status to determine if USB has been disconnected. * If we use USB online status to determine replug, we will need to * extend the delay between re-enabling CC detection and checking the * USB online status. */ ret = GPSY_GET_PROP(ovh_info->usb_psy, POWER_SUPPLY_PROP_PRESENT); if (ret < 0) return ret; ovh_info->usb_connected = ret; #ifdef CONFIG_USB_ONLINE_IS_TYPEC_MODE ret = GPSY_GET_INT_PROP(ovh_info->usb_psy, POWER_SUPPLY_PROP_TYPEC_MODE, &mode); #else ret = GPSY_GET_INT_PROP(ovh_info->usb_psy, POWER_SUPPLY_PROP_ONLINE, &mode); #endif pr_debug("%s: TYPEC mode=%d ret=%d\n", __func__, mode, ret); if (ret < 0) return ret; ovh_info->accessory_connected = (mode == POWER_SUPPLY_TYPEC_SINK) || (mode == POWER_SUPPLY_TYPEC_SINK_POWERED_CABLE); curr_state = ovh_info->usb_connected || ovh_info->accessory_connected; if (curr_state && !prev_state) { ovh_info->plug_time = get_seconds_since_boot(); ovh_info->plug_temp = ovh_info->temp; } if (ovh_info->overheat_mitigation) { ret = gvotable_cast_bool_vote(ovh_info->disable_power_role_switch, USB_OVERHEAT_MITIGATION_VOTER, true); if (ret < 0) { dev_err(ovh_info->dev, "Couldn't vote for disable_power_role_switch ret=%d\n", ret); return ret; } } if (curr_state != prev_state) dev_info(ovh_info->dev, "USB is %sconnected", curr_state ? "" : "dis"); // USB should be disconnected for two cycles before replug is acked if (ovh_info->overheat_mitigation && !curr_state && !prev_state) ovh_info->usb_replug = true; return 0; } static inline int get_usb_port_temp(struct overheat_info *ovh_info) { int temp; temp = GPSY_GET_PROP(ovh_info->usb_psy, POWER_SUPPLY_PROP_TEMP); if (temp == -EINVAL || temp == -ENODATA) return temp; dev_info(ovh_info->dev, "Update USB port temp:%d\n", temp); if (temp > ovh_info->max_temp) ovh_info->max_temp = temp; ovh_info->temp = temp; return 0; } 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 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 (get_usb_port_temp(ovh_info) < 0) goto rerun; ret = update_usb_status(ovh_info); if (ret < 0) goto rerun; if (ovh_info->overheat_mitigation && (!mitigation_enabled || (ovh_info->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 && mitigation_enabled && ovh_info->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->throttle_state) goto rerun; // Do not run again, USB port isn't overheated 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 usb_get_cur_state(struct thermal_cooling_device *cooling_dev, unsigned long *state) { struct overheat_info *ovh_info = cooling_dev->devdata; if (!ovh_info) return -EINVAL; *state = ovh_info->throttle_state; return 0; } static int usb_get_max_state(struct thermal_cooling_device *cooling_dev, unsigned long *state) { *state = USB_MAX_THROTTLE_STATE; return 0; } static int usb_set_cur_state(struct thermal_cooling_device *cooling_dev, unsigned long state) { struct overheat_info *ovh_info = cooling_dev->devdata; unsigned long current_state; if (!ovh_info) return -EINVAL; if (state > USB_MAX_THROTTLE_STATE) return -EINVAL; current_state = ovh_info->throttle_state; ovh_info->throttle_state = state; if (current_state != state) { dev_info(ovh_info->dev, "usb overheat throttle state=%lu\n", state); mod_delayed_work(system_wq, &ovh_info->port_overheat_work, 0); } return 0; } static const struct thermal_cooling_device_ops usb_cooling_ops = { .get_max_state = usb_get_max_state, .get_cur_state = usb_get_cur_state, .set_cur_state = usb_set_cur_state, }; static int ovh_probe(struct platform_device *pdev) { int ret = 0; struct overheat_info *ovh_info; struct power_supply *usb_psy; struct gvotable_election *usb_icl_votable; struct gvotable_election *disable_power_role_switch; usb_psy = power_supply_get_by_name("usb"); if (!usb_psy) return -EPROBE_DEFER; usb_icl_votable = gvotable_election_get_handle("USB_ICL"); if (usb_icl_votable == NULL) { pr_err("Couldn't find USB_ICL votable\n"); return -EPROBE_DEFER; } disable_power_role_switch = gvotable_election_get_handle("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->max_temp = INT_MIN; ovh_info->plug_temp = INT_MIN; ret = get_dts_vars(ovh_info); if (ret < 0) return -ENODEV; // initialize votables gvotable_cast_int_vote(ovh_info->usb_icl_votable, USB_OVERHEAT_MITIGATION_VOTER, 0, false); gvotable_cast_bool_vote(ovh_info->disable_power_role_switch, USB_OVERHEAT_MITIGATION_VOTER, false); ovh_info->overheat_ws = wakeup_source_register(NULL, "overheat_mitigation"); if (!ovh_info->overheat_ws) { dev_err(ovh_info->dev, "%s: failed to get wakeup source\n", __func__); return -ENODEV; } INIT_DELAYED_WORK(&ovh_info->port_overheat_work, port_overheat_work); // register power supply change notifier to update usb metric data 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; } /* Register cooling device */ ovh_info->cooling_dev = thermal_of_cooling_device_register( dev_of_node(ovh_info->dev), "usb-port", ovh_info, &usb_cooling_ops); if (IS_ERR(ovh_info->cooling_dev)) { ret = PTR_ERR(ovh_info->cooling_dev); dev_err(ovh_info->dev, "%s: failed to register cooling device: %d\n", __func__, ret); return ret; } platform_set_drvdata(pdev, ovh_info); ret = sysfs_create_group(&ovh_info->dev->kobj, &ovh_attr_group); if (ret < 0) { dev_err(ovh_info->dev, "Cannot create sysfs group, ret=%d\n", ret); } return 0; } static int ovh_remove(struct platform_device *pdev) { struct overheat_info *ovh_info = platform_get_drvdata(pdev); if (ovh_info) { power_supply_unreg_notifier(&ovh_info->psy_nb); sysfs_remove_group(&ovh_info->dev->kobj, &ovh_attr_group); wakeup_source_unregister(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 "); MODULE_LICENSE("GPL v2");