blob: 5d106ce78ca4b673d88b7cd5eb34cec6d07ebb82 [file] [log] [blame]
/* 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 <linux/module.h>
#include <linux/of.h>
#include <linux/delay.h>
#include <linux/ktime.h>
#include <linux/math64.h>
#include <linux/platform_device.h>
#include <linux/printk.h>
#include <linux/thermal.h>
#include <misc/gvotable.h>
#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 <maggiewhite@google.com>");
MODULE_LICENSE("GPL v2");