Files
kernel_google_redbull/drivers/usb/pd/pd_engine.c
Lucas Wei 3509ed2a2f Merge android-msm-pixel-4.19-sc-qpr3 into android-msm-pixel-4.19
SBMerger: 410055097
Change-Id: I72d1a6ee6761476964cba04731509b4e6e1089df
Signed-off-by: SecurityBot <android-nexus-securitybot@system.gserviceaccount.com>
Signed-off-by: Lucas Wei <lucaswei@google.com>
2022-02-17 10:39:22 +08:00

2700 lines
66 KiB
C

/*
* Copyright 2016-2017 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/debugfs.h>
#include <linux/device.h>
#include <../../extcon/extcon.h>
#include <linux/delay.h>
#include <linux/extcon.h>
#include <linux/jiffies.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/pmic-voter.h>
#include <linux/power_supply.h>
#include <linux/property.h>
#include <linux/regulator/consumer.h>
#include <linux/rtc.h>
#include <linux/sched/clock.h>
#include <linux/seq_file.h>
#include <linux/slab.h>
#include <linux/types.h>
#include <linux/usb/typec.h>
#include <linux/usb/pd.h>
#include <linux/usb/tcpm.h>
#include <linux/vmalloc.h>
#include <linux/workqueue.h>
#include "usbpd.h"
#include <../../power/supply/google/logbuffer.h>
#include <../../power/supply/qcom/smb5-reg.h>
#define LOG_BUFFER_ENTRIES 1024
#define LOG_BUFFER_ENTRY_SIZE 256
#define EXT_VBUS_WORK_DELAY_MS 5000
#define EXT_VBUS_OVERLAP_MS 7
#define CMDLINE_BOOT_MODE_KEY "androidboot.mode"
#define CHARGINGTEST_BOOT_MODE "chargingtest"
#define CHARGER_BOOT_MODE "charger"
#define CMDLINE_SUZYQ_KEY "usbcfg.suzyq"
#define SUZYQ_ENABLED "enabled"
#define CMDLINE_PARAM(k, v) ( k"="v )
#define OTG_ICL_VOTER "OTG_ICL_VOTER"
#define OTG_DISABLE_APSD_VOTER "OTG_DISABLE_APSD_VOTER"
#define TCPM_DISABLE_CC_VOTER "TCPM_DISABLE_CC_VOTER"
#define LIMIT_USB_VOTER "LIMIT_USB_VOTER"
#define DEFAULT_SRC_CAP_VOTER "DEFAULT_SRC_CAP_VOTER"
#define WIRELESS_SRC_CAP_VOTER "WIRLESS_SRC_CAP_VOTER"
#define LIMIT_SRC_CAP_VOTER "LIMIT_SRC_CAP_VOTER"
#define MAX_SRC_CUR_MA 900
#define DEFAULT_SRC_CUR_MA MAX_SRC_CUR_MA
#define WIRELESS_SRC_CUR_MA 500
#define LIMIT_SRC_CUR_MA 0
static char boot_mode_string[64];
static char suzyq_enabled[15];
struct usbpd {
struct device dev;
struct tcpm_port *tcpm_port;
struct tcpc_dev tcpc_dev;
struct pd_phy_params pdphy_params;
bool pdphy_open;
struct extcon_dev *extcon;
struct power_supply *usb_psy;
struct power_supply *wireless_psy;
struct notifier_block psy_nb;
struct regulator *vbus;
struct regulator *vconn;
struct regulator *ext_vbus;
struct notifier_block hw_reg_nb;
bool vbus_output;
bool external_vbus;
bool external_vbus_update;
bool vconn_output;
bool vbus_present;
enum power_supply_type psy_type;
enum typec_cc_status cc1;
enum typec_cc_status cc2;
bool is_cable_flipped;
bool pending_update_usb_data;
bool extcon_usb;
bool extcon_usb_host;
bool extcon_usb_cc;
struct mutex lock; /* struct usbpd access lock */
struct workqueue_struct *wq;
enum typec_role cur_pwr_role;
enum typec_data_role cur_data_role;
bool in_hard_reset;
/* debugfs logging */
struct dentry *rootdir;
bool in_pr_swap;
bool apsd_done;
bool pd_capable;
/* Ext vbus management */
struct delayed_work ext_vbus_work;
struct notifier_block ext_vbus_nb;
struct votable *usb_icl_votable;
struct votable *apsd_disable_votable;
/* Acutual regulator status */
bool smb2_vbus_reg;
bool ext_vbus_reg;
/* Indicates whether the device has to honor usb suspend power limits*/
bool suspend_supported;
bool usb_comm_capable;
bool ext_vbus_supported;
bool wlc_supported;
bool wireless_online;
bool low_power_udev;
bool switch_based_on_maxpower;
bool fixed_pdo_5V3A;
bool limit_sink_enable;
unsigned int limit_sink_current; /* uA */
bool in_explicit_contract;
/* alternate source capabilities */
struct work_struct update_pdo_work;
bool default_src_cap;
u32 src_pdo[PDO_MAX_OBJECTS];
unsigned int nr_src_pdo;
bool limit_src_cap_enable;
struct votable *src_cap_votable;
/* logging client */
struct logbuffer *log;
bool pd_disabled;
struct votable *disable_power_role_switch;
};
static u8 always_enable_data;
/*
* Logging
*/
#ifdef CONFIG_DEBUG_FS
static int pd_engine_debugfs_init(struct usbpd *pd)
{
if (!pd->rootdir) {
pd->rootdir = debugfs_create_dir("pd_engine", NULL);
if (!pd->rootdir)
return -ENOMEM;
}
if (!debugfs_create_u8("always_enable_data",
0600, pd->rootdir,
&always_enable_data)) {
dev_err(&pd->dev, "Failed to create data path debug");
return -EAGAIN;
}
return 0;
}
static void pd_engine_debugfs_exit(struct usbpd *pd)
{
debugfs_remove_recursive(pd->rootdir);
}
#endif /* CONFIG_DEBUG_FS */
static const char * const get_typec_mode_name(
enum power_supply_typec_mode typec_mode)
{
switch (typec_mode) {
case POWER_SUPPLY_TYPEC_NONE:
return "NONE";
case POWER_SUPPLY_TYPEC_SOURCE_DEFAULT:
return "SOURCE_DEFAULT";
case POWER_SUPPLY_TYPEC_SOURCE_MEDIUM:
return "SOURCE_MEDIUM";
case POWER_SUPPLY_TYPEC_SOURCE_HIGH:
return "SOURCE_HIGH";
case POWER_SUPPLY_TYPEC_DAM_DEFAULT:
return "DAM_DEFAULT";
case POWER_SUPPLY_TYPEC_DAM_MEDIUM:
return "DAM_MEDIUM";
case POWER_SUPPLY_TYPEC_DAM_HIGH:
return "DAM_HIGH";
case POWER_SUPPLY_TYPEC_NON_COMPLIANT:
return "NON_COMPLIANT";
case POWER_SUPPLY_TYPEC_RP_STD_STD:
return "RP_STD_STD";
case POWER_SUPPLY_TYPEC_RP_MED_MED:
return "RP_MED_MED";
case POWER_SUPPLY_TYPEC_RP_HIGH_HIGH:
return "RP_HIGH_HIGH";
case POWER_SUPPLY_TYPEC_SINK:
return "SINK";
case POWER_SUPPLY_TYPEC_SINK_POWERED_CABLE:
return "SINK_POWERED_CABLE";
case POWER_SUPPLY_TYPEC_SINK_DEBUG_ACCESSORY:
return "SINK_DEBUG_ACCESSORY";
case POWER_SUPPLY_TYPEC_SINK_AUDIO_ADAPTER:
return "SINK_AUDIO_ADAPTER";
case POWER_SUPPLY_TYPEC_POWERED_CABLE_ONLY:
return "POWERED_CABLE_ONLY";
default:
return "UNDEFINED";
}
}
static const char * const get_psy_type_name(enum power_supply_type psy_type)
{
switch (psy_type) {
case POWER_SUPPLY_TYPE_UNKNOWN:
return "UNKNOWN";
case POWER_SUPPLY_TYPE_USB:
return "SDP";
case POWER_SUPPLY_TYPE_USB_CDP:
return "CDP";
case POWER_SUPPLY_TYPE_USB_DCP:
return "DCP";
case POWER_SUPPLY_TYPE_USB_HVDCP:
return "HVDCP2";
case POWER_SUPPLY_TYPE_USB_HVDCP_3:
return "HVDCP3";
case POWER_SUPPLY_TYPE_USB_PD:
return "PD";
default:
return "UNDEFINED";
}
}
/* must align with smblib's integer representation of cc orientation. */
enum typec_cc_orientation {
TYPEC_CC_ORIENTATION_NONE = 0,
TYPEC_CC_ORIENTATION_CC1 = 1,
TYPEC_CC_ORIENTATION_CC2 = 2,
};
static const char * const get_typec_cc_orientation_name(
enum typec_cc_orientation typec_cc_orientation)
{
switch (typec_cc_orientation) {
case TYPEC_CC_ORIENTATION_NONE:
return "NONE";
case TYPEC_CC_ORIENTATION_CC1:
return "CC1";
case TYPEC_CC_ORIENTATION_CC2:
return "CC2";
default:
return "UNDEFINED";
}
}
static const char * const get_typec_cc_status_name(
enum typec_cc_status cc_status)
{
switch (cc_status) {
case TYPEC_CC_OPEN:
return "OPEN";
case TYPEC_CC_RA:
return "Ra";
case TYPEC_CC_RD:
return "Rd";
case TYPEC_CC_RP_DEF:
return "Rp-def";
case TYPEC_CC_RP_1_5:
return "Rp-1.5";
case TYPEC_CC_RP_3_0:
return "Rp-3.0";
default:
return "UNDEFINED";
}
}
/*
* parses the type-c mode to cc pin status in the orientation_none special case.
*/
static void parse_cc_status_none_orientation(
enum power_supply_typec_mode typec_mode,
enum typec_cc_status *cc1,
enum typec_cc_status *cc2)
{
switch (typec_mode) {
case POWER_SUPPLY_TYPEC_SINK_DEBUG_ACCESSORY:
*cc1 = *cc2 = TYPEC_CC_RD;
break;
case POWER_SUPPLY_TYPEC_SINK_AUDIO_ADAPTER:
*cc1 = *cc2 = TYPEC_CC_RA;
break;
case POWER_SUPPLY_TYPEC_RP_STD_STD:
*cc1 = *cc2 = TYPEC_CC_RP_DEF;
break;
case POWER_SUPPLY_TYPEC_RP_MED_MED:
*cc1 = *cc2 = TYPEC_CC_RP_1_5;
break;
case POWER_SUPPLY_TYPEC_RP_HIGH_HIGH:
*cc1 = *cc2 = TYPEC_CC_RP_3_0;
break;
default:
*cc1 = *cc2 = TYPEC_CC_OPEN;
break;
}
}
/*
* parses the type-c mode to the status of the active and inactive cc pin.
*/
static void parse_cc_status_active_inactive(
enum power_supply_typec_mode typec_mode,
enum typec_cc_status *cc_active,
enum typec_cc_status *cc_inactive)
{
switch (typec_mode) {
case POWER_SUPPLY_TYPEC_NONE:
*cc_active = TYPEC_CC_OPEN;
*cc_inactive = TYPEC_CC_OPEN;
break;
case POWER_SUPPLY_TYPEC_SOURCE_DEFAULT:
*cc_active = TYPEC_CC_RP_DEF;
*cc_inactive = TYPEC_CC_OPEN;
break;
case POWER_SUPPLY_TYPEC_SOURCE_MEDIUM:
*cc_active = TYPEC_CC_RP_1_5;
*cc_inactive = TYPEC_CC_OPEN;
break;
case POWER_SUPPLY_TYPEC_SOURCE_HIGH:
*cc_active = TYPEC_CC_RP_3_0;
*cc_inactive = TYPEC_CC_OPEN;
break;
case POWER_SUPPLY_TYPEC_DAM_DEFAULT:
*cc_active = TYPEC_CC_RP_3_0;
*cc_inactive = TYPEC_CC_RP_1_5;
break;
case POWER_SUPPLY_TYPEC_DAM_MEDIUM:
*cc_active = TYPEC_CC_RP_1_5;
*cc_inactive = TYPEC_CC_RP_DEF;
break;
case POWER_SUPPLY_TYPEC_DAM_HIGH:
*cc_active = TYPEC_CC_RP_3_0;
*cc_inactive = TYPEC_CC_RP_DEF;
break;
case POWER_SUPPLY_TYPEC_NON_COMPLIANT:
*cc_active = TYPEC_CC_OPEN;
*cc_inactive = TYPEC_CC_OPEN;
break;
case POWER_SUPPLY_TYPEC_SINK:
*cc_active = TYPEC_CC_RD;
*cc_inactive = TYPEC_CC_OPEN;
break;
case POWER_SUPPLY_TYPEC_SINK_POWERED_CABLE:
*cc_active = TYPEC_CC_RD;
*cc_inactive = TYPEC_CC_RA;
break;
case POWER_SUPPLY_TYPEC_SINK_DEBUG_ACCESSORY:
*cc_active = TYPEC_CC_RD;
*cc_inactive = TYPEC_CC_RD;
break;
case POWER_SUPPLY_TYPEC_SINK_AUDIO_ADAPTER:
*cc_active = TYPEC_CC_RA;
*cc_inactive = TYPEC_CC_RA;
break;
case POWER_SUPPLY_TYPEC_POWERED_CABLE_ONLY:
*cc_active = TYPEC_CC_RA;
*cc_inactive = TYPEC_CC_OPEN;
break;
default:
*cc_active = TYPEC_CC_OPEN;
*cc_inactive = TYPEC_CC_OPEN;
break;
}
}
static void parse_cc_status(enum power_supply_typec_mode typec_mode,
enum typec_cc_orientation typec_cc_orientation,
enum typec_cc_status *cc1,
enum typec_cc_status *cc2)
{
enum typec_cc_status cc_active, cc_inactive;
/* handle orientation_none special cases */
if (typec_cc_orientation == TYPEC_CC_ORIENTATION_NONE) {
parse_cc_status_none_orientation(typec_mode, cc1, cc2);
return;
}
parse_cc_status_active_inactive(typec_mode, &cc_active, &cc_inactive);
/* assign cc1, cc2 status based on orientation */
if (typec_cc_orientation == TYPEC_CC_ORIENTATION_CC1) {
*cc1 = cc_active;
*cc2 = cc_inactive;
} else if (typec_cc_orientation == TYPEC_CC_ORIENTATION_CC2) {
*cc1 = cc_inactive;
*cc2 = cc_active;
} else {
*cc1 = *cc2 = TYPEC_CC_OPEN;
}
}
static inline bool psy_support_usb_data(enum power_supply_type psy_type)
{
return (psy_type == POWER_SUPPLY_TYPE_USB) ||
(psy_type == POWER_SUPPLY_TYPE_USB_CDP);
}
static int update_extcon_prop(struct usbpd *pd, int extcon_type)
{
int ret;
union extcon_property_value val;
val.intval = pd->extcon_usb_cc ? 1 : 0;
ret = extcon_set_property(pd->extcon, extcon_type,
EXTCON_PROP_USB_TYPEC_POLARITY,
val);
if (ret < 0) {
logbuffer_log(pd->log,
"unable to set extcon usb polarity prop [%s], ret=%d",
pd->extcon_usb_cc ? "Y" : "N", ret);
return ret;
}
/*
* When wirless charging is on and attempting to start host mode,
* degrade to HS as internal pmic cannot be on.
*/
if (pd->wireless_online && extcon_type == EXTCON_USB_HOST)
val.intval = 0;
else
val.intval = 1;
ret = extcon_set_property(pd->extcon, extcon_type,
EXTCON_PROP_USB_SS,
val);
if (ret < 0) {
logbuffer_log(pd->log,
"unable to set extcon usb ss prop, ret=%d",
ret);
return ret;
}
if (extcon_type == EXTCON_USB) {
val.intval = pd->pd_capable ? 1 : 0;
ret = extcon_set_property(pd->extcon, extcon_type,
EXTCON_PROP_USB_TYPEC_MED_HIGH_CURRENT,
val);
if (ret < 0) {
logbuffer_log(pd->log,
"unable to set extcon usb typec med high current, ret=%d",
ret);
}
}
return ret;
}
static int update_usb_data_role(struct usbpd *pd)
{
int ret;
bool extcon_usb;
/* Turn on USB data (device role) only when a valid power supply that
* supports USB data connection is connected, i.e. SDP or CDP
*
* update_usb_data_role() is called when APSD is done, thus here
* pd->psy_type is a valid detection result.
*/
extcon_usb = pd->extcon_usb &&
(psy_support_usb_data(pd->psy_type) ||
pd->usb_comm_capable ||
always_enable_data);
/*
* EXTCON_USB_HOST and EXTCON_USB is mutually exlusive.
* Therefore while responding to commands such as DR_SWAP,
* update the cable state of the one being turned off before
* turning on the other one.
*/
if (!pd->extcon_usb_host) {
ret = extcon_set_state_sync(pd->extcon, EXTCON_USB_HOST,
pd->extcon_usb_host);
if (ret < 0) {
logbuffer_log(pd->log,
"unable to turn off extcon usb host, ret=%d",
ret);
return ret;
}
}
if (!extcon_usb) {
ret = extcon_set_state_sync(pd->extcon, EXTCON_USB,
extcon_usb);
if (ret < 0) {
logbuffer_log(pd->log,
"unable to turn off extcon usb device, ret=%d",
ret);
return ret;
}
}
/*
* Shouldnt be turning on extcon_usb and extcon_usb_host
* at the same time.
*/
BUG_ON(extcon_usb && pd->extcon_usb_host);
if (extcon_usb || pd->extcon_usb_host) {
ret = update_extcon_prop(pd, extcon_usb ?
EXTCON_USB : EXTCON_USB_HOST);
if (ret < 0)
return ret;
ret = extcon_set_state_sync(pd->extcon, extcon_usb ?
EXTCON_USB : EXTCON_USB_HOST,
1);
if (ret < 0) {
logbuffer_log(pd->log,
"unable to turn on extcon [%s], ret=%d",
extcon_usb ? "device" : "host", ret);
return ret;
}
}
logbuffer_log(pd->log,
"usb extcon: cc [%s], host [%s], usb [%s]",
pd->extcon_usb_cc ? "Y" : "N",
pd->extcon_usb_host ? "Y" : "N",
extcon_usb ? "Y" : "N");
return ret;
}
struct psy_changed_event {
struct work_struct work;
struct usbpd *pd;
};
static int pd_regulator_update(struct usbpd *pd, bool external_reg, bool on)
{
int ret = 0;
if (on) {
if (external_reg)
ret = regulator_set_voltage(pd->ext_vbus,
5000000, 5000000);
ret = regulator_enable(external_reg ? pd->ext_vbus
: pd->vbus);
if (ret) {
logbuffer_log(pd->log,
"update_vbus_locked: unable to turn on %s vbus ret = %d"
, external_reg ? "external" : "pmic"
, ret);
return ret;
} else {
logbuffer_log(pd->log,
"update_vbus_locked: turned on %s vbus ret = %d"
, external_reg ? "external" : "pmic"
, ret);
}
} else {
ret = regulator_disable(external_reg ? pd->ext_vbus
: pd->vbus);
if (ret) {
logbuffer_log(pd->log,
"update_vbus_locked: unable to turn off %s vbus ret = %d"
, external_reg ? "external" : "pmic"
, ret);
return ret;
} else {
logbuffer_log(pd->log,
"update_vbus_locked: turned off %s vbus ret = %d"
, external_reg ? "external" : "pmic"
, ret);
}
}
if (external_reg)
pd->ext_vbus_reg = on;
else
pd->smb2_vbus_reg = on;
return ret;
}
static int update_vbus_locked(struct usbpd *pd, bool vbus_output)
{
int ret = 0;
if (pd->vbus_output == vbus_output)
return ret;
pd->external_vbus = pd->wireless_online ? true : false;
logbuffer_log(pd->log,
"%s: vbus_output: %c wireless_online: %c"
, __func__
, vbus_output ? 'Y' : 'N', pd->wireless_online ?
'Y' : 'N');
if (vbus_output) {
ret = pd_regulator_update(pd, pd->external_vbus, true);
if (ret)
return ret;
} else {
if (pd->ext_vbus_reg) {
ret = pd_regulator_update(pd, true, false);
if (ret)
return ret;
}
if (pd->smb2_vbus_reg) {
ret = pd_regulator_update(pd, false, false);
if (ret)
return ret;
}
}
pd->vbus_output = vbus_output;
schedule_work(&pd->update_pdo_work);
/*
* No interrupt will be trigerred for the vbus change.
* Manually inform tcpm to check the vbus change.
*/
tcpm_vbus_change(pd->tcpm_port);
return ret;
}
void update_external_vbus(struct work_struct *work)
{
int ret = 0;
struct usbpd *pd = container_of(to_delayed_work(work), struct usbpd,
ext_vbus_work);
mutex_lock(&pd->lock);
if (!pd->ext_vbus_supported)
goto err;
/* Exit when the old value is same as the new value or
* update the value and exit when vbus is not on.
*/
if (pd->external_vbus == pd->external_vbus_update || !pd->vbus_output) {
logbuffer_log(pd->log,
"skipping update value changed: %c vbus_output: %c"
, pd->external_vbus == pd->external_vbus_update ?
'Y' : 'N', pd->vbus_output ? 'Y' : 'N');
goto exit;
}
if (pd->wireless_online) {
logbuffer_log(pd->log,
"skipping update as wireless charger online");
goto err;
}
/*
* Turn on the other regulator before turning off the other one.
*/
ret = pd_regulator_update(pd, pd->external_vbus_update, true);
if (ret)
goto err;
/* Regulator overlap according to hardware recommendation */
msleep(EXT_VBUS_OVERLAP_MS);
ret = pd_regulator_update(pd, !pd->external_vbus_update, false);
if (ret)
goto err;
exit:
pd->external_vbus = pd->external_vbus_update;
schedule_work(&pd->update_pdo_work);
err:
pm_relax(&pd->dev);
mutex_unlock(&pd->lock);
}
static int fast_role_swap_set(struct usbpd *pd, bool enable)
{
union power_supply_propval val = {0};
int ret;
val.intval = enable ? 1 : 0;
ret = power_supply_set_property(pd->usb_psy,
POWER_SUPPLY_PROP_OTG_FASTROLESWAP,
&val);
if (ret < 0) {
logbuffer_log(pd->log, "unable to %s FASTROLESWAP, ret=%d",
enable ? "set" : "clear", ret);
}
return ret;
}
static int update_wireless_locked(struct usbpd *pd, bool wireless_online)
{
int ret = 0;
if (wireless_online == pd->wireless_online)
return 0;
pd->wireless_online = wireless_online;
logbuffer_log(pd->log,
"pd->vbus_output: %c pd->external_vbus: %c low_power:%c",
pd->vbus_output ? 'Y' : 'N',
pd->external_vbus ? 'Y' : 'N',
pd->low_power_udev ? 'Y' : 'N');
/*
* When partner is pd capable update the source caps and force
* disconnect when wireless turns online.
* Wait for vbus off/disconnect and wireless offline
* to fall back to default source caps.
*/
if (pd->pd_capable) {
/* Setting different src_cap depending on wireless status */
schedule_work(&pd->update_pdo_work);
if (pd->wireless_online && pd->vbus_output
&& !pd->external_vbus) {
/* Turn off internal vbus */
if (pd_regulator_update(pd, false, false))
return -EAGAIN;
pd->vbus_output = false;
/* Force disconnect */
tcpm_port_reset(pd->tcpm_port);
}
return ret;
}
/* Wireless charging enabled when high power usb device is connected */
if (pd->vbus_output && !pd->external_vbus
&& pd->wireless_online && !pd->low_power_udev) {
/* Turn off internal vbus */
if (pd_regulator_update(pd, false, false))
return -EAGAIN;
/* Force disconnect to reduce power requirement to USB 2.0*/
/* disable host mode */
pd->extcon_usb_host = false;
if (update_usb_data_role(pd))
return -EAGAIN;
/* Turn on external vbus */
if (pd_regulator_update(pd, true, true))
return -EAGAIN;
pd->external_vbus = true;
/* enable host mode */
pd->extcon_usb_host = true;
if (update_usb_data_role(pd))
return -EAGAIN;
logbuffer_log(pd->log,
"psy_changed: swiched to external vbus");
/* Wireless charging enabled when low power usb device is connected */
} else if (pd->vbus_output && !pd->external_vbus
&& pd->wireless_online && pd->low_power_udev) {
/* Turn on external vbus */
if (pd_regulator_update(pd, true, true))
return -EAGAIN;
pd->external_vbus = true;
msleep(EXT_VBUS_OVERLAP_MS);
/* Turn off internal vbus */
if (pd_regulator_update(pd, false, false))
return -EAGAIN;
/* Wireless charging disabled. Switch back to internal pmic when
* wireless charging is disabled.
*/
} else if (!pd->wireless_online && pd->vbus_output
&& pd->external_vbus) {
fast_role_swap_set(pd, true);
/* Turn on internal vbus */
if (pd_regulator_update(pd, false, true)) {
ret = -EAGAIN;
goto clear;
}
pd->external_vbus = false;
msleep(EXT_VBUS_OVERLAP_MS);
/* Turn off external vbus */
if (pd_regulator_update(pd, true, false))
ret = -EAGAIN;
clear:
fast_role_swap_set(pd, false);
}
return ret;
}
#define PDO_FIXED_FLAGS \
(PDO_FIXED_DUAL_ROLE | PDO_FIXED_SUSPEND \
| PDO_FIXED_DATA_SWAP | PDO_FIXED_USB_COMM)
static void update_src_caps(struct work_struct *work)
{
struct usbpd *pd = container_of(work, struct usbpd, update_pdo_work);
if (pd->wireless_online && pd->default_src_cap) {
logbuffer_log(pd->log, "alternative src_cap");
vote(pd->src_cap_votable, WIRELESS_SRC_CAP_VOTER, true,
WIRELESS_SRC_CUR_MA);
pd->default_src_cap = false;
} else if (!pd->wireless_online &&
!pd->vbus_output &&
!pd->default_src_cap) {
logbuffer_log(pd->log, "default src_cap");
vote(pd->src_cap_votable, WIRELESS_SRC_CAP_VOTER, false, 0);
pd->default_src_cap = true;
}
}
static int pd_switch_src_cap_callback(struct votable *votable, void *data,
int cur_ma,
const char *client)
{
struct usbpd *pd = data;
unsigned int nr_pdo;
u32 *pdo_array, pdo_limited;
int ret;
if (!pd->tcpm_port || cur_ma < 0)
return 0;
if (cur_ma > MAX_SRC_CUR_MA)
cur_ma = MAX_SRC_CUR_MA;
logbuffer_log(pd->log, "src_cap current set to %d mA", cur_ma);
if (cur_ma == DEFAULT_SRC_CUR_MA) {
pdo_array = pd->src_pdo;
nr_pdo = pd->nr_src_pdo;
} else {
pdo_array = &pdo_limited;
pdo_limited = pd->src_pdo[0];
pdo_limited &= ~(PDO_CURR_MASK << PDO_FIXED_CURR_SHIFT);
pdo_limited |= PDO_FIXED_CURR(cur_ma);
nr_pdo = 1;
}
ret = tcpm_update_source_capabilities(pd->tcpm_port, pdo_array, nr_pdo);
if (ret < 0)
logbuffer_log(pd->log, "Couldn't update src cap %d", ret);
return ret;
}
static void psy_changed_handler(struct work_struct *work)
{
struct psy_changed_event *event = container_of(work,
struct psy_changed_event,
work);
struct usbpd *pd = event->pd;
bool vbus_present;
enum typec_cc_status cc1;
enum typec_cc_status cc2;
enum power_supply_type psy_type;
enum power_supply_typec_mode typec_mode;
enum typec_cc_orientation typec_cc_orientation;
bool pe_start;
union power_supply_propval val;
bool wireless_online = false;
int ret = 0;
pm_wakeup_event(&pd->dev, PD_ACTIVITY_TIMEOUT_MS);
ret = power_supply_get_property(pd->usb_psy,
POWER_SUPPLY_PROP_TYPEC_MODE, &val);
if (ret < 0) {
logbuffer_log(pd->log, "Unable to read TYPEC_MODE, ret=%d",
ret);
goto free;
}
typec_mode = val.intval;
ret = power_supply_get_property(pd->usb_psy,
POWER_SUPPLY_PROP_PE_START, &val);
if (ret < 0) {
logbuffer_log(pd->log, "Unable to read PE_START, ret=%d",
ret);
goto free;
}
pe_start = !!val.intval;
ret = power_supply_get_property(pd->usb_psy,
POWER_SUPPLY_PROP_REAL_TYPE, &val);
if (ret < 0) {
logbuffer_log(pd->log, "Unable to read TYPE, ret=%d", ret);
return;
}
psy_type = val.intval;
ret = power_supply_get_property(pd->usb_psy,
POWER_SUPPLY_PROP_PRESENT, &val);
if (ret < 0) {
logbuffer_log(pd->log, "Unable to read ONLINE, ret=%d",
ret);
goto free;
}
vbus_present = val.intval;
ret = power_supply_get_property(pd->usb_psy,
POWER_SUPPLY_PROP_TYPEC_CC_ORIENTATION,
&val);
if (ret < 0) {
logbuffer_log(pd->log,
"Unable to read TYPEC_CC_ORIENTATION, ret=%d",
ret);
goto free;
}
typec_cc_orientation = val.intval;
if (pd->wlc_supported) {
ret = power_supply_get_property(pd->wireless_psy,
POWER_SUPPLY_PROP_ONLINE,
&val);
if (ret < 0) {
logbuffer_log(pd->log,
"Unable to read wireless online property, ret=%d",
ret);
goto free;
}
wireless_online = val.intval ? true : false;
}
parse_cc_status(typec_mode, typec_cc_orientation, &cc1, &cc2);
logbuffer_log(pd->log,
"type [%s], pe_start [%s], vbus_present [%s], mode [%s], orientation [%s], cc1 [%s], cc2 [%s], external_vbus_update [%s], wireless_online [%s]",
get_psy_type_name(psy_type),
pe_start ? "Y" : "N",
vbus_present ? "Y" : "N",
get_typec_mode_name(typec_mode),
get_typec_cc_orientation_name(typec_cc_orientation),
get_typec_cc_status_name(cc1),
get_typec_cc_status_name(cc2),
pd->external_vbus_update ? "Y" : "N",
wireless_online ? "Y" : "N");
mutex_lock(&pd->lock);
if (update_wireless_locked(pd, wireless_online))
goto unlock;
/* Dont proceed as pmi might still be evaluating connections */
if (!pe_start && !pd->pd_capable &&
psy_type == POWER_SUPPLY_TYPE_UNKNOWN &&
typec_mode != POWER_SUPPLY_TYPEC_NONE &&
typec_mode != POWER_SUPPLY_TYPEC_SINK_AUDIO_ADAPTER) {
logbuffer_log(pd->log,
"Skipping update as PE_START not set yet");
goto unlock;
}
pd->apsd_done = !!psy_type;
pd->is_cable_flipped = typec_cc_orientation == TYPEC_CC_ORIENTATION_CC2;
pd->extcon_usb_cc = pd->is_cable_flipped;
if (vbus_present != pd->vbus_present) {
logbuffer_log(pd->log, "vbus present: %s -> %s",
pd->vbus_present ? "True" : "False",
vbus_present ? "True" : "False");
pd->vbus_present = vbus_present;
tcpm_vbus_change(pd->tcpm_port);
}
if (vbus_present && (pd->psy_type == POWER_SUPPLY_TYPE_USB &&
psy_type == POWER_SUPPLY_TYPE_USB_DCP))
pd->pending_update_usb_data = true;
pd->psy_type = psy_type;
if (cc1 != pd->cc1 || cc2 != pd->cc2) {
logbuffer_log(pd->log, "cc1: %s -> %s, cc2: %s -> %s",
get_typec_cc_status_name(pd->cc1),
get_typec_cc_status_name(cc1),
get_typec_cc_status_name(pd->cc2),
get_typec_cc_status_name(cc2));
/* fake CC for SuzyQ cables */
if (!strncmp(suzyq_enabled, SUZYQ_ENABLED,
strlen(SUZYQ_ENABLED)) &&
typec_mode == POWER_SUPPLY_TYPEC_DAM_MEDIUM)
parse_cc_status(POWER_SUPPLY_TYPEC_SOURCE_DEFAULT,
typec_cc_orientation, &cc1, &cc2);
pd->cc1 = cc1;
pd->cc2 = cc2;
tcpm_cc_change(pd->tcpm_port);
}
if (pd->apsd_done && pd->pending_update_usb_data) {
logbuffer_log(pd->log,
"APSD is done now, update pending usb data role");
ret = update_usb_data_role(pd);
if (ret < 0)
goto unlock;
pd->pending_update_usb_data = false;
}
unlock:
mutex_unlock(&pd->lock);
free:
kfree(event);
}
static int psy_changed(struct notifier_block *nb, unsigned long evt, void *ptr)
{
struct usbpd *pd;
struct psy_changed_event *event;
pd = container_of(nb, struct usbpd, psy_nb);
if (ptr == pd->wireless_psy)
logbuffer_log(pd->log, "wireless supply changed");
if (!((ptr == pd->usb_psy || ptr == pd->wireless_psy)
&& evt == PSY_EVENT_PROP_CHANGED))
return 0;
else
logbuffer_log(pd->log, "supply changed\n");
event = kzalloc(sizeof(*event), GFP_ATOMIC);
if (!event)
return -ENOMEM;
INIT_WORK(&event->work, psy_changed_handler);
event->pd = pd;
queue_work(pd->wq, &event->work);
return 0;
}
static int tcpm_init(struct tcpc_dev *dev)
{
return 0;
}
static int tcpm_get_vbus(struct tcpc_dev *dev)
{
struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev);
bool vbus_on;
int ret;
mutex_lock(&pd->lock);
vbus_on = pd->vbus_present || pd->vbus_output;
logbuffer_log(pd->log, "%s: %s", __func__,
vbus_on ? "True" : "False");
ret = vbus_on ? 1 : 0;
mutex_unlock(&pd->lock);
return ret;
}
#define cc_is_sink(cc) \
((cc) == TYPEC_CC_RP_DEF || (cc) == TYPEC_CC_RP_1_5 || \
(cc) == TYPEC_CC_RP_3_0)
#define port_is_sink(port) \
((cc_is_sink((port)->cc1) && !cc_is_sink((port)->cc2)) || \
(cc_is_sink((port)->cc2) && !cc_is_sink((port)->cc1)))
#define cc_is_source(cc) ((cc) == TYPEC_CC_RD)
#define port_is_source(port) \
((cc_is_source((port)->cc1) && \
!cc_is_source((port)->cc2)) || \
(cc_is_source((port)->cc2) && \
!cc_is_source((port)->cc1)))
static int tcpm_set_cc(struct tcpc_dev *dev, enum typec_cc_status cc)
{
union power_supply_propval val = {0};
union power_supply_propval rp_val = {0};
struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev);
int ret = 0;
mutex_lock(&pd->lock);
switch (cc) {
case TYPEC_CC_OPEN:
break;
case TYPEC_CC_RD:
val.intval = POWER_SUPPLY_TYPEC_PR_SINK;
break;
case TYPEC_CC_RP_DEF:
val.intval = POWER_SUPPLY_TYPEC_PR_SOURCE;
rp_val.intval = TYPEC_SRC_RP_STD;
break;
case TYPEC_CC_RP_1_5:
val.intval = POWER_SUPPLY_TYPEC_PR_SOURCE;
rp_val.intval = TYPEC_SRC_RP_1P5A;
break;
case TYPEC_CC_RP_3_0:
val.intval = POWER_SUPPLY_TYPEC_PR_SOURCE;
rp_val.intval = TYPEC_SRC_RP_3A;
break;
default:
logbuffer_log(pd->log, "%s: invalid cc %s", __func__,
get_typec_cc_status_name(cc));
ret = -EINVAL;
goto unlock;
}
if (cc == TYPEC_CC_RP_DEF ||
cc == TYPEC_CC_RP_1_5 ||
cc == TYPEC_CC_RP_3_0) {
ret = power_supply_set_property(pd->usb_psy,
POWER_SUPPLY_PROP_TYPEC_SRC_RP,
&rp_val);
if (ret < 0) {
logbuffer_log(pd->log, "unable to set Rp, ret=%d",
ret);
} else {
logbuffer_log(pd->log, "set Rp to %s",
get_typec_cc_status_name(cc));
}
}
/*
* Setting POWER_ROLE will cause CC to Open in the first place.
* Skip the setting if unnecessary
*/
if ((port_is_source(pd) && cc_is_sink(cc)) ||
(port_is_sink(pd) && cc_is_source(cc)))
goto unlock;
/*
* Vote true to ensure that CC will not be set toggling by others and at
* the same time CC will be set to Open in the callback of the votable.
*
* Vote false to remove the vote of TCPM_DISABLE_CC_VOTER. If the voting
* result is changed, it will set the power role to PR_DUAL in smblib.
*/
ret = vote(pd->disable_power_role_switch, TCPM_DISABLE_CC_VOTER,
cc == TYPEC_CC_OPEN ? true : false, 0);
if (ret < 0) {
logbuffer_log(pd->log,
"unable to vote DISABLE_POWER_ROLE_SWITCH to %s, ret=%d",
cc == TYPEC_CC_OPEN ? true : false, ret);
goto unlock;
}
/*
* power role would be set in the callback of DISABLE_POWER_ROLE_SWITCH
* if we are going to set cc to Open.
*/
if (cc != TYPEC_CC_OPEN) {
ret = power_supply_set_property(pd->usb_psy,
POWER_SUPPLY_PROP_TYPEC_POWER_ROLE,
&val);
if (ret < 0)
logbuffer_log(pd->log,
"unable to set POWER_ROLE to %d, ret=%d",
cc, ret);
else
logbuffer_log(pd->log, "set POWER_ROLE to %d", cc);
} else {
logbuffer_log(pd->log, "set POWER_ROLE to CC_OPEN");
}
unlock:
mutex_unlock(&pd->lock);
return ret;
}
static int tcpm_get_cc(struct tcpc_dev *dev, enum typec_cc_status *cc1,
enum typec_cc_status *cc2)
{
struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev);
mutex_lock(&pd->lock);
logbuffer_log(pd->log, "%s: cc1=%s, cc2=%s", __func__,
get_typec_cc_status_name(pd->cc1),
get_typec_cc_status_name(pd->cc2));
*cc1 = pd->cc1;
*cc2 = pd->cc2;
mutex_unlock(&pd->lock);
return 0;
}
static int tcpm_set_polarity(struct tcpc_dev *dev,
enum typec_cc_polarity polarity)
{
/* Hardware handles polarity, no op here. */
return 0;
}
static int tcpm_set_vconn(struct tcpc_dev *dev, bool on)
{
struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev);
int ret = 0;
mutex_lock(&pd->lock);
if (on != pd->vconn_output) {
if (on)
ret = regulator_enable(pd->vconn);
else
ret = regulator_disable(pd->vconn);
if (ret < 0) {
logbuffer_log(pd->log,
"unable to turn %s vconn, ret=%d",
on ? "on" : "off", ret);
goto unlock;
}
pd->vconn_output = on;
}
logbuffer_log(pd->log, "set vconn: %s", on ? "on" : "off");
unlock:
mutex_unlock(&pd->lock);
return ret;
}
static int tcpm_set_vbus(struct tcpc_dev *dev, bool on, bool charge)
{
struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev);
int ret = 0;
int usb_icl;
bool work_flushed, apsd_disabled;
work_flushed = flush_delayed_work(&pd->ext_vbus_work);
logbuffer_log(pd->log, "Flushed ext vbus delayed work: %s",
work_flushed ? "yes" : "no");
mutex_lock(&pd->lock);
if (on) {
/* disable charging */
ret = vote(pd->usb_icl_votable, OTG_ICL_VOTER, true, 0);
if (ret < 0) {
logbuffer_log(pd->log, "vote usb_icl 0 fail, ret %d",
ret);
goto unlock;
}
/* disable APSD */
ret = vote(pd->apsd_disable_votable,
OTG_DISABLE_APSD_VOTER, true, 0);
if (ret < 0) {
logbuffer_log(pd->log, "vote apsd_disable fail, ret %d",
ret);
goto vote_icl_default;
}
} else {
/* enable APSD */
ret = vote(pd->apsd_disable_votable,
OTG_DISABLE_APSD_VOTER, false, 0);
if (ret < 0) {
logbuffer_log(pd->log,
"unvote apsd_disable fail, ret %d",
ret);
goto vote_icl_default;
}
/* enable charging */
ret = vote(pd->usb_icl_votable, OTG_ICL_VOTER, false, 0);
if (ret < 0) {
logbuffer_log(pd->log, "unvote usb_icl fail, ret %d",
ret);
goto unlock;
}
}
apsd_disabled = get_effective_result(pd->apsd_disable_votable);
usb_icl = get_effective_result(pd->usb_icl_votable);
if (on && !apsd_disabled) {
logbuffer_log(pd->log, "apsd not disabled, icl = %d", usb_icl);
goto vote_apsd_default;
}
ret = update_vbus_locked(pd, on);
if (ret)
goto vote_apsd_default;
logbuffer_log(pd->log, "set vbus: %s, apsd: %s, icl: %d",
on ? "on" : "off",
apsd_disabled ? "disabled" : "enabled",
usb_icl);
goto unlock;
vote_apsd_default:
/* enable APSD by default */
ret = vote(pd->apsd_disable_votable,
OTG_DISABLE_APSD_VOTER, false, 0);
if (ret < 0)
logbuffer_log(pd->log, "unvote apsd_disable fail, ret %d", ret);
vote_icl_default:
/* enable charging by default */
ret = vote(pd->usb_icl_votable, OTG_ICL_VOTER, false, 0);
if (ret < 0)
logbuffer_log(pd->log, "unvote usb_icl fail, ret %d", ret);
unlock:
mutex_unlock(&pd->lock);
return ret;
}
static int tcpm_set_current_limit(struct tcpc_dev *dev, u32 max_ma, u32 mv)
{
union power_supply_propval val = {0};
struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev);
int ret = 0;
if (!pd->pd_capable)
return 0;
val.intval = mv * 1000;
ret = power_supply_set_property(pd->usb_psy,
POWER_SUPPLY_PROP_PD_VOLTAGE_MAX,
&val);
if (ret < 0) {
logbuffer_log(pd->log,
"unable to set max voltage to %d, ret=%d",
mv, ret);
return ret;
}
val.intval = max_ma * 1000;
ret = power_supply_set_property(pd->usb_psy,
POWER_SUPPLY_PROP_PD_CURRENT_MAX,
&val);
if (ret < 0) {
logbuffer_log(pd->log,
"unable to set pd current max to %d, ret=%d",
max_ma, ret);
return ret;
}
logbuffer_log(pd->log, "max_ma=%d, mv=%d", max_ma, mv);
return 0;
}
static int tcpm_set_pd_rx(struct tcpc_dev *dev, bool on, int filter)
{
struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev);
int ret = 0;
if (pd->pd_disabled)
return -EINVAL;
if (on) {
ret = pd_phy_update_frame_filter(filter);
if (ret < 0) {
logbuffer_log(pd->log,
"unable to set frame filter %d: ret=%d",
filter, ret);
return ret;
}
}
if (pd->pdphy_open == on) {
logbuffer_log(pd->log, "pd_phy already %s",
on ? "open" : "closed");
return 0;
}
if (on) {
ret = pd_phy_open(&pd->pdphy_params);
if (ret < 0) {
logbuffer_log(pd->log, "unable to %s pd_phy, ret=%d",
on ? "open" : "close", ret);
return ret;
}
} else {
/* pd_phy_close() has no return value. */
pd_phy_close();
}
pd->pdphy_open = on;
logbuffer_log(pd->log, "%s pd_phy", on ? "open" : "close");
return 0;
}
static int get_data_len(__le16 header)
{
int ret;
ret = pd_header_cnt(__le16_to_cpu(header));
ret *= 4;
return ret;
}
static const char * const get_tcpm_transmit_type_name(
enum tcpm_transmit_type type)
{
switch (type) {
case TCPC_TX_SOP:
return "SOP";
case TCPC_TX_SOP_PRIME:
return "SOP_PRIME";
case TCPC_TX_SOP_PRIME_PRIME:
return "SOP_PRIME_PRIME";
case TCPC_TX_SOP_DEBUG_PRIME:
return "SOP_DEBUG_PRIME";
case TCPC_TX_SOP_DEBUG_PRIME_PRIME:
return "SOP_DEBUG_PRIME_PRIME";
case TCPC_TX_HARD_RESET:
return "HARD_RESET";
case TCPC_TX_CABLE_RESET:
return "CABLE_RESET";
case TCPC_TX_BIST_MODE_2:
return "BIST_MODE_2";
default:
return "UNDEFINED";
}
}
struct pd_transmit_work {
struct work_struct work;
struct usbpd *pd;
enum tcpm_transmit_type type;
struct pd_message msg;
};
static const char * const get_tcpm_transmit_status_name(
enum tcpm_transmit_status status)
{
switch (status) {
case TCPC_TX_SUCCESS:
return "SUCCESS";
case TCPC_TX_DISCARDED:
return "DISCARDED";
case TCPC_TX_FAILED:
return "FAILED";
default:
return "UNDEFINED";
}
}
static void pd_transmit_handler(struct work_struct *work)
{
struct pd_transmit_work *pd_tx_work = container_of(work,
struct pd_transmit_work,
work);
struct usbpd *pd = pd_tx_work->pd;
enum tcpm_transmit_type type = pd_tx_work->type;
struct pd_message *msg = &pd_tx_work->msg;
enum tcpm_transmit_status status;
bool signal;
int ret = 0;
switch (type) {
case TCPC_TX_HARD_RESET:
ret = pd_phy_signal(HARD_RESET_SIG);
signal = true;
break;
case TCPC_TX_CABLE_RESET:
ret = pd_phy_signal(CABLE_RESET_SIG);
signal = true;
break;
case TCPC_TX_SOP:
ret = pd_phy_write(msg->header, (u8 *)msg->payload,
get_data_len(msg->header),
SOP_MSG);
signal = false;
break;
case TCPC_TX_SOP_PRIME:
ret = pd_phy_write(msg->header, (u8 *)msg->payload,
get_data_len(msg->header),
SOPI_MSG);
signal = false;
break;
case TCPC_TX_SOP_PRIME_PRIME:
ret = pd_phy_write(msg->header, (u8 *)msg->payload,
get_data_len(msg->header),
SOPII_MSG);
signal = false;
break;
default:
logbuffer_log(pd->log, "unknown pd tx type");
kfree(pd_tx_work);
return;
}
/*
* pd_phy_write()/pd_phy_signal() return value for hw irq event:
* - tx succeeded hw irq: 0 for signal or actual tx len for write
* - tx discarded hw irq: -EBUSY
* - tx failed hw irq: -EFAULT
*/
if (ret >= 0)
status = TCPC_TX_SUCCESS;
else if (ret == -EBUSY)
status = TCPC_TX_DISCARDED;
else
status = TCPC_TX_FAILED;
tcpm_pd_transmit_complete(pd->tcpm_port, status);
if (signal)
logbuffer_log(pd->log,
"pd tx type [%s], pdphy ret [%d], status [%s]",
get_tcpm_transmit_type_name(type),
ret, get_tcpm_transmit_status_name(status));
else
logbuffer_log(pd->log,
"pd tx header [%#x], type [%s], pdphy ret [%d], status [%s]",
msg->header, get_tcpm_transmit_type_name(type),
ret, get_tcpm_transmit_status_name(status));
kfree(pd_tx_work);
}
static int tcpm_pd_transmit(struct tcpc_dev *dev, enum tcpm_transmit_type type,
const struct pd_message *msg)
{
struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev);
struct pd_transmit_work *pd_tx_work;
if (pd->pd_disabled)
return -EINVAL;
switch (type) {
case TCPC_TX_HARD_RESET:
case TCPC_TX_CABLE_RESET:
case TCPC_TX_SOP:
case TCPC_TX_SOP_PRIME:
case TCPC_TX_SOP_PRIME_PRIME:
break;
default:
logbuffer_log(pd->log, "unsupported pd type: %s",
get_tcpm_transmit_type_name(type));
return -EINVAL;
}
pd_tx_work = kzalloc(sizeof(*pd_tx_work), GFP_ATOMIC);
if (!pd_tx_work)
return -ENOMEM;
INIT_WORK(&pd_tx_work->work, pd_transmit_handler);
pd_tx_work->pd = pd;
pd_tx_work->type = type;
if (msg)
memcpy(&pd_tx_work->msg, msg, sizeof(*msg));
queue_work(pd->wq, &pd_tx_work->work);
if (msg)
logbuffer_log(pd->log, "queue pd tx header [%#x], type [%s]",
msg->header, get_tcpm_transmit_type_name(type));
else
logbuffer_log(pd->log, "queue pd tx type [%s]",
get_tcpm_transmit_type_name(type));
return 0;
}
static int tcpm_start_toggling(struct tcpc_dev *dev,
enum typec_port_type port_type,
enum typec_cc_status cc)
{
/*
* Ignore the typec_cc_status for now. As current no
* src_pdo is configured, the default is Rp_def.
*/
struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev);
int ret;
if (port_type != TYPEC_PORT_DRP)
return -EOPNOTSUPP;
mutex_lock(&pd->lock);
ret = vote(pd->disable_power_role_switch, TCPM_DISABLE_CC_VOTER,
false, 0);
if (ret < 0) {
logbuffer_log(pd->log,
"unable to unvote DISABLE_POWER_ROLE_SWITCH, ret=%d",
ret);
goto unlock;
}
ret = rerun_election(pd->disable_power_role_switch);
if (ret < 0)
logbuffer_log(pd->log,
"error rerunning election of DISABLE_POWER_ROLE_SWITCH, ret=%d",
ret);
logbuffer_log(pd->log, "start toggling, cc is %s",
get_effective_result(pd->disable_power_role_switch) ?
"disabled" : "toggling");
/* Force a recheck of the CC status since cc lines are
* assumed to be open.
*/
psy_changed(&pd->psy_nb, PSY_EVENT_PROP_CHANGED, pd->usb_psy);
unlock:
mutex_unlock(&pd->lock);
return ret;
}
static int tcpm_set_in_pr_swap(struct tcpc_dev *dev, bool pr_swap)
{
union power_supply_propval val = {0};
struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev);
int ret = 0;
mutex_lock(&pd->lock);
if (pd->in_pr_swap != pr_swap) {
if (!pr_swap) {
val.intval = POWER_SUPPLY_TYPEC_PR_DUAL;
ret = power_supply_set_property(pd->usb_psy,
POWER_SUPPLY_PROP_TYPEC_POWER_ROLE,
&val);
if (ret < 0) {
logbuffer_log(pd->log,
"unable to set POWER_ROLE to PR_DUAL, ret=%d",
ret);
goto unlock;
}
/* Required for the PMIC to recover */
logbuffer_log(pd->log, "sleeping for 20mec");
msleep(20);
psy_changed(&pd->psy_nb, PSY_EVENT_PROP_CHANGED,
pd->usb_psy);
}
val.intval = pr_swap ? 1 : 0;
ret = power_supply_set_property(pd->usb_psy,
POWER_SUPPLY_PROP_PR_SWAP,
&val);
if (ret < 0) {
logbuffer_log(pd->log,
"unable to set PR_SWAP to %d, ret=%d",
pr_swap ? 1 : 0, ret);
goto unlock;
}
pd->in_pr_swap = pr_swap;
logbuffer_log(pd->log, "PR_SWAP = %d", pr_swap ? 1 : 0);
}
unlock:
mutex_unlock(&pd->lock);
return ret;
}
static int tcpm_set_suspend_supported(struct tcpc_dev *dev,
bool suspend_supported)
{
union power_supply_propval val = {0};
struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev);
int ret = 0;
mutex_lock(&pd->lock);
if (suspend_supported == pd->suspend_supported)
goto unlock;
/* Attempt once */
pd->suspend_supported = suspend_supported;
val.intval = suspend_supported ? 1 : 0;
logbuffer_log(pd->log, "usb suspend %d", suspend_supported ? 1 : 0);
ret = power_supply_set_property(pd->usb_psy,
POWER_SUPPLY_PROP_PD_USB_SUSPEND_SUPPORTED,
&val);
if (ret < 0) {
logbuffer_log(pd->log,
"unable to set suspend flag to %d, ret=%d",
suspend_supported ? 1 : 0, ret);
}
unlock:
mutex_unlock(&pd->lock);
return ret;
}
enum power_role get_pdphy_power_role(enum typec_role role)
{
switch (role) {
case TYPEC_SINK:
return PR_SINK;
case TYPEC_SOURCE:
return PR_SRC;
default:
return PR_NONE;
}
}
static const char * const get_typec_role_name(enum typec_role role)
{
switch (role) {
case TYPEC_SINK:
return "SINK";
case TYPEC_SOURCE:
return "SOURCE";
default:
return "UNDEFINED";
}
}
static const char * const get_pdphy_pr_name(enum power_role pdphy_pr)
{
switch (pdphy_pr) {
case PR_SINK:
return "PR_SINK";
case PR_SRC:
return "PR_SRC";
case PR_NONE:
return "PR_NONE";
default:
return "UNDEFINED";
}
}
enum data_role get_pdphy_data_role(enum typec_data_role data)
{
switch (data) {
case TYPEC_DEVICE:
return DR_UFP;
case TYPEC_HOST:
return DR_DFP;
default:
return DR_NONE;
}
}
static const char * const get_typec_data_role_name(enum typec_data_role data)
{
switch (data) {
case TYPEC_DEVICE:
return "DEVICE";
case TYPEC_HOST:
return "HOST";
default:
return "UNDEFINED";
}
}
static const char * const get_pdphy_dr_name(enum data_role pdphy_dr)
{
switch (pdphy_dr) {
case DR_UFP:
return "DR_UFP";
case DR_DFP:
return "DR_DFP";
case DR_NONE:
return "DR_NONE";
default:
return "UNDEFINED";
}
}
static int set_pd_header(struct usbpd *pd, enum typec_role role,
enum typec_data_role data)
{
enum power_role pdphy_pr;
enum data_role pdphy_dr;
int ret = 0;
pdphy_pr = get_pdphy_power_role(role);
pdphy_dr = get_pdphy_data_role(data);
ret = pd_phy_update_roles(pdphy_dr, pdphy_pr);
if (ret < 0) {
logbuffer_log(pd->log,
"unable to set pd_phy_header: %s, %s, ret=%d",
get_pdphy_pr_name(pdphy_pr),
get_pdphy_dr_name(pdphy_dr),
ret);
return ret;
}
logbuffer_log(pd->log, "set pd_phy_header: %s, %s",
get_pdphy_pr_name(pdphy_pr),
get_pdphy_dr_name(pdphy_dr));
return 0;
}
#define EXTCON_USB_SUPER_SPEED true
static int set_usb_data_role(struct usbpd *pd, bool attached,
enum typec_role role,
enum typec_data_role data,
bool usb_comm_capable)
{
int ret = 0;
pd->extcon_usb_cc = pd->is_cable_flipped;
if (!attached) {
pd->extcon_usb = false;
pd->extcon_usb_host = false;
} else if (data == TYPEC_HOST) {
pd->extcon_usb = false;
pd->extcon_usb_host = true;
} else {
pd->extcon_usb = true;
pd->extcon_usb_host = false;
}
pd->usb_comm_capable = usb_comm_capable;
logbuffer_log(pd->log,
"set usb_data_role: power [%s], data [%s], apsd_done [%s], attached [%s], comm [%s]",
get_typec_role_name(role),
get_typec_data_role_name(data),
pd->apsd_done ? "Y" : "N",
attached ? "Y" : "N",
usb_comm_capable ? "Y" : "N");
if (attached && role == TYPEC_SINK &&
!(pd->apsd_done || usb_comm_capable || always_enable_data)) {
/* wait for APSD done */
logbuffer_log(pd->log,
"APSD is not done, delay update usb data role");
pd->pending_update_usb_data = true;
} else {
ret = update_usb_data_role(pd);
if (ret < 0)
return ret;
pd->pending_update_usb_data = false;
}
return ret;
}
static int tcpm_set_roles(struct tcpc_dev *dev, bool attached,
enum typec_role role, enum typec_data_role data,
bool usb_comm_capable)
{
struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev);
int ret;
mutex_lock(&pd->lock);
ret = set_pd_header(pd, role, data);
if (ret < 0)
goto unlock;
ret = set_usb_data_role(pd, attached, role, data, usb_comm_capable);
if (!ret) {
pd->cur_pwr_role = role;
pd->cur_data_role = data;
pd->pdphy_params.power_role = get_pdphy_power_role(role);
pd->pdphy_params.data_role = get_pdphy_data_role(data);
}
unlock:
mutex_unlock(&pd->lock);
return ret;
}
static void pd_phy_signal_rx(struct usbpd *pd, enum pd_sig_type type)
{
switch (type) {
case HARD_RESET_SIG:
tcpm_pd_hard_reset(pd->tcpm_port);
logbuffer_log(pd->log, "received pd hard reset");
break;
default:
logbuffer_log(pd->log, "received unsupported signal: %d",
type);
return;
}
}
static void pd_phy_message_rx(struct usbpd *pd, enum pd_sop_type sop,
u8 *buf, size_t len)
{
struct pd_message msg;
if (len < 2) {
logbuffer_log(pd->log, "invalid message received, len=%ld",
len);
return;
}
msg.header = cpu_to_le16(*((u16 *)buf));
buf += sizeof(u16);
len -= sizeof(u16);
if (get_data_len(msg.header) != len) {
logbuffer_log(pd->log, "header len %d != len %ld",
get_data_len(msg.header), len);
return;
}
if (len > PD_MAX_PAYLOAD * 4) {
logbuffer_log(pd->log, "len %ld > PD_MAX_PAYLOAD", len);
return;
}
memcpy(msg.payload, buf, len);
logbuffer_log(pd->log, "pd rx header [%#x]", msg.header);
tcpm_pd_receive(pd->tcpm_port, &msg, (enum usb_pd_sop_type) sop);
}
static void set_in_hard_reset_locked(struct tcpc_dev *dev, bool status)
{
union power_supply_propval val = {0};
struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev);
int ret = 0;
if (status == pd->in_hard_reset)
return;
val.intval = status ? 1 : 0;
ret = power_supply_set_property(pd->usb_psy,
POWER_SUPPLY_PROP_PD_IN_HARD_RESET,
&val);
if (ret < 0) {
logbuffer_log(pd->log,
"unable to set hard reset status to %s, ret=%d",
status ? "true" : "false", ret);
return;
}
pd->in_hard_reset = status;
}
static void pd_phy_shutdown(struct usbpd *pd)
{
int rc = 0;
flush_delayed_work(&pd->ext_vbus_work);
mutex_lock(&pd->lock);
/* silently signal a hard_reset to reset port partner */
if (pd->pd_capable && pd->vbus_present) {
set_in_hard_reset_locked(&pd->tcpc_dev, true);
tcpm_set_current_limit(&pd->tcpc_dev, 0, 5000);
tcpm_pd_transmit(&pd->tcpc_dev, TCPC_TX_HARD_RESET, NULL);
}
pd->pd_disabled = true;
if (regulator_is_enabled(pd->vbus)) {
rc = regulator_disable(pd->vbus);
if (rc < 0) {
pr_err("unable to disable vbus\n");
} else {
pd->vbus_output = false;
pd->smb2_vbus_reg = false;
}
}
if (pd->ext_vbus_reg) {
rc = regulator_disable(pd->ext_vbus);
if (rc < 0) {
pr_err("unable to disable vbus\n");
} else {
pd->vbus_output = false;
pd->ext_vbus_reg = false;
}
}
if (regulator_is_enabled(pd->vconn)) {
rc = regulator_disable(pd->vconn);
if (rc < 0)
pr_err("unable to disable vconn\n");
else
pd->vconn_output = false;
}
mutex_unlock(&pd->lock);
pr_debug("pd phy shutdown\n");
}
static void log_rtc(struct tcpc_dev *dev)
{
struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev);
logbuffer_log(pd->log, "");
return;
}
static void set_pd_capable(struct tcpc_dev *dev, bool capable)
{
union power_supply_propval val = {0};
struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev);
int ret = 0;
if (pd->pd_capable == capable && !pd->in_pr_swap)
return;
val.intval = capable ? POWER_SUPPLY_PD_ACTIVE :
POWER_SUPPLY_PD_INACTIVE;
pd->pd_capable = capable;
ret = power_supply_set_property(pd->usb_psy,
POWER_SUPPLY_PROP_PD_ACTIVE,
&val);
if (ret < 0) {
logbuffer_log(pd->log, "unable to set pd capable to %s, ret=%d",
capable ? "true" : "false", ret);
}
}
static void set_in_hard_reset(struct tcpc_dev *dev, bool status)
{
struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev);
mutex_lock(&pd->lock);
set_in_hard_reset_locked(dev, status);
mutex_unlock(&pd->lock);
}
static const struct tcpc_config pd_tcpc_config = {
.alt_modes = NULL,
.try_role_hw = true,
};
static const struct tcpc_config pd_tcpc_config_charger = {
.alt_modes = NULL,
.try_role_hw = true,
.type = TYPEC_PORT_SNK,
};
static int init_tcpc_dev(struct usbpd *pd,
struct device *parent)
{
struct tcpc_dev *pd_tcpc_dev = &pd->tcpc_dev;
int ret = 0;
pd_tcpc_dev->fwnode = device_get_named_child_node(parent,
"connector");
if (!pd_tcpc_dev->fwnode) {
dev_err(&pd->dev, "Can't find connector node.\n");
return -EINVAL;
}
/* Get source pdos */
ret = fwnode_property_read_u32_array(pd_tcpc_dev->fwnode, "source-pdos",
NULL, 0);
if (ret <= 0)
return -EINVAL;
pd->nr_src_pdo = min(ret, PDO_MAX_OBJECTS);
fwnode_property_read_u32_array(pd_tcpc_dev->fwnode, "source-pdos",
pd->src_pdo, pd->nr_src_pdo);
pd_tcpc_dev->config = &pd_tcpc_config;
pd_tcpc_dev->init = tcpm_init;
pd_tcpc_dev->get_vbus = tcpm_get_vbus;
pd_tcpc_dev->set_cc = tcpm_set_cc;
pd_tcpc_dev->get_cc = tcpm_get_cc;
pd_tcpc_dev->set_polarity = tcpm_set_polarity;
pd_tcpc_dev->set_vconn = tcpm_set_vconn;
pd_tcpc_dev->set_vbus = tcpm_set_vbus;
pd_tcpc_dev->set_current_limit = tcpm_set_current_limit;
pd_tcpc_dev->set_pd_rx = tcpm_set_pd_rx;
pd_tcpc_dev->set_roles = tcpm_set_roles;
pd_tcpc_dev->try_role = NULL;
pd_tcpc_dev->pd_transmit = tcpm_pd_transmit;
pd_tcpc_dev->start_toggling = tcpm_start_toggling;
pd_tcpc_dev->set_in_pr_swap = tcpm_set_in_pr_swap;
pd_tcpc_dev->set_pd_capable = set_pd_capable;
pd_tcpc_dev->set_in_hard_reset = set_in_hard_reset;
pd_tcpc_dev->log_rtc = log_rtc;
pd_tcpc_dev->set_suspend_supported = tcpm_set_suspend_supported;
pd_tcpc_dev->fixed_5V3A = false;
if (!strncmp(boot_mode_string, CHARGER_BOOT_MODE,
strlen(CHARGER_BOOT_MODE))) {
pd_tcpc_dev->port_type_override = true;
pd_tcpc_dev->config = &pd_tcpc_config_charger;
} else {
pd_tcpc_dev->port_type_override = false;
}
return 0;
}
static void init_pd_phy_params(struct pd_phy_params *pdphy_params)
{
pdphy_params->signal_cb = pd_phy_signal_rx;
pdphy_params->msg_rx_cb = pd_phy_message_rx;
pdphy_params->shutdown_cb = pd_phy_shutdown;
pdphy_params->data_role = DR_UFP;
pdphy_params->power_role = PR_SINK;
pdphy_params->frame_filter_val = FRAME_FILTER_EN_SOP |
FRAME_FILTER_EN_HARD_RESET;
}
static int update_ext_vbus(struct notifier_block *self, unsigned long action,
void *dev)
{
struct usbpd *pd = container_of(self, struct usbpd, ext_vbus_nb);
bool turn_on_ext_vbus = (action == EXT_VBUS_ON) ? true : false;
bool work_queued, work_cancelled;
work_cancelled = cancel_delayed_work_sync(&pd->ext_vbus_work);
logbuffer_log(pd->log, "ext_vbus_work_cancelled: %s",
work_cancelled ? "yes" : "no");
if (work_cancelled)
pm_relax(&pd->dev);
mutex_lock(&pd->lock);
if (!pd->ext_vbus_supported)
goto exit;
pd->low_power_udev = turn_on_ext_vbus;
if (pd->switch_based_on_maxpower) {
if (pd->pd_capable) {
logbuffer_log(pd->log,
"PD is capable, don't operate ext_vbus");
goto exit;
}
pd->external_vbus_update = turn_on_ext_vbus;
work_queued = queue_delayed_work(pd->wq, &pd->ext_vbus_work,
turn_on_ext_vbus ?
msecs_to_jiffies(EXT_VBUS_WORK_DELAY_MS)
: 0);
if (!work_queued)
logbuffer_log(pd->log,
"error: queueing ext_vbus_work failed");
else {
pm_stay_awake(&pd->dev);
logbuffer_log(pd->log, "queued work EXT_VBUS_%s",
(action == EXT_VBUS_ON) ?
"ON" : "OFF");
}
}
exit:
mutex_unlock(&pd->lock);
return NOTIFY_OK;
}
static int ext_boost_changed(struct notifier_block *nb, unsigned long evt,
void *data)
{
struct usbpd *pd = container_of(nb, struct usbpd, hw_reg_nb);
if (evt == REGULATOR_EVENT_ENABLE)
logbuffer_log(pd->log, "ext_boost enabled");
else if (evt == REGULATOR_EVENT_DISABLE)
logbuffer_log(pd->log, "ext_boost disabled");
return NOTIFY_OK;
}
static bool search_param(char *cmdline, const char *search_str)
{
char *found_str;
char *temp_str = cmdline;
found_str = strnstr(temp_str, search_str, strlen(temp_str));
return found_str ? true : false;
}
static void parse_cmdline()
{
char *cmdline;
const char *chargingtest = CMDLINE_PARAM(CMDLINE_BOOT_MODE_KEY,
CHARGINGTEST_BOOT_MODE);
const char *charger = CMDLINE_PARAM(CMDLINE_BOOT_MODE_KEY,
CHARGER_BOOT_MODE);
const char *suzyq = CMDLINE_PARAM(CMDLINE_SUZYQ_KEY, SUZYQ_ENABLED);
cmdline = kstrdup(saved_command_line, GFP_KERNEL);
if (!cmdline)
return;
if (search_param(cmdline, chargingtest)) {
always_enable_data = 1;
strlcpy(boot_mode_string, CHARGINGTEST_BOOT_MODE,
sizeof(boot_mode_string));
} else if (search_param(cmdline, charger)) {
strlcpy(boot_mode_string, CHARGER_BOOT_MODE,
sizeof(boot_mode_string));
}
if (search_param(cmdline, suzyq))
strlcpy(suzyq_enabled, SUZYQ_ENABLED, sizeof(suzyq_enabled));
kfree(cmdline);
}
static const unsigned int usbpd_extcon_cable[] = {
EXTCON_USB,
EXTCON_USB_HOST,
EXTCON_DISP_DP,
EXTCON_NONE,
};
static void usbpd_release(struct device *dev)
{
struct usbpd *pd = container_of(dev, struct usbpd, dev);
kfree(pd);
}
static int num_pd_instances;
static ssize_t pdo_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
struct usbpd *pd = container_of(dev, struct usbpd, dev);
struct tcpc_dev *pd_tcpc_dev = &pd->tcpc_dev;
return scnprintf(buf, PAGE_SIZE, "%d\n",
pd_tcpc_dev->fixed_5V3A);
}
static ssize_t pdo_store(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count)
{
struct usbpd *pd = container_of(dev, struct usbpd, dev);
struct tcpc_dev *pd_tcpc_dev = &pd->tcpc_dev;
if (sysfs_streq(buf, "5V3A") && pd->fixed_pdo_5V3A) {
pd_tcpc_dev->fixed_5V3A = true;
tcpm_port_reset(pd->tcpm_port);
} else {
pd_tcpc_dev->fixed_5V3A = false;
}
return count;
}
static DEVICE_ATTR_RW(pdo);
static ssize_t usb_limit_sink_enable_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct usbpd *pd = container_of(dev, struct usbpd, dev);
return sysfs_emit(buf, "%u\n", pd->limit_sink_enable);
}
/*
* usb_limit_sink_current has to be set before usb_limit_sink_enable is
* invoked.
*/
static ssize_t usb_limit_sink_enable_store(struct device *dev,
struct device_attribute *attr,
const char *buf,
size_t count)
{
struct usbpd *pd = container_of(dev, struct usbpd, dev);
bool enable;
int ret;
if (kstrtobool(buf, &enable) < 0)
return -EINVAL;
logbuffer_log(pd->log, "usb_limit_sink_enable %u (%u uA)", enable,
pd->limit_sink_current);
if (enable) {
ret = vote(pd->usb_icl_votable, "LIMIT_USB_VOTER", true,
pd->limit_sink_current);
if (ret < 0) {
dev_err(&pd->dev, "Cannot set limit sink current, ret %d\n",
ret);
goto exit;
}
} else {
ret = vote(pd->usb_icl_votable, "LIMIT_USB_VOTER", false, 0);
if (ret < 0) {
dev_err(&pd->dev, "Cannot unvote for sink current, ret %d\n",
ret);
goto exit;
}
}
pd->limit_sink_enable = enable;
exit:
return count;
}
static DEVICE_ATTR_RW(usb_limit_sink_enable);
static ssize_t usb_limit_sink_current_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct usbpd *pd = container_of(dev, struct usbpd, dev);
return sysfs_emit(buf, "%u\n", pd->limit_sink_current);
}
/*
* usb_icl will not be updated if limit_sink_enable is already enabled.
* unit uA
*/
static ssize_t usb_limit_sink_current_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct usbpd *pd = container_of(dev, struct usbpd, dev);
unsigned int val;
if (kstrtouint(buf, 0, &val) < 0)
return -EINVAL;
/* Never accept current over 3A */
if (val > 3000000)
return -EINVAL;
pd->limit_sink_current = val;
return count;
}
static DEVICE_ATTR_RW(usb_limit_sink_current);
static ssize_t usb_limit_source_enable_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct usbpd *pd = container_of(dev, struct usbpd, dev);
return sysfs_emit(buf, "%u\n", pd->limit_src_cap_enable);
}
static ssize_t usb_limit_source_enable_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct usbpd *pd = container_of(dev, struct usbpd, dev);
bool enable;
if (kstrtobool(buf, &enable) < 0)
return -EINVAL;
if (enable)
vote(pd->src_cap_votable, LIMIT_SRC_CAP_VOTER, true,
LIMIT_SRC_CUR_MA);
else
vote(pd->src_cap_votable, LIMIT_SRC_CAP_VOTER, false, 0);
pd->limit_src_cap_enable = enable;
return count;
}
static DEVICE_ATTR_RW(usb_limit_source_enable);
static struct device_attribute *usbpd_device_attrs[] = {
&dev_attr_pdo,
&dev_attr_usb_limit_sink_enable,
&dev_attr_usb_limit_sink_current,
&dev_attr_usb_limit_source_enable,
NULL
};
/**
* usbpd_create - Create a new instance of USB PD protocol/policy engine
* @parent - parent device to associate with
*
* This creates a new usbpd class device which manages the state of a
* USB PD-capable port. The parent device that is passed in should be
* associated with the physical device port, e.g. a PD PHY.
*
* Derived from policy_engine.c.
*
* Return: struct usbpd pointer, or an ERR_PTR value
*/
struct usbpd *usbpd_create(struct device *parent)
{
int ret = 0, i;
struct usbpd *pd;
union power_supply_propval val = {0};
pd = kzalloc(sizeof(*pd), GFP_KERNEL);
if (!pd)
return ERR_PTR(-ENOMEM);
pd->log = logbuffer_register("usbpd");
if (IS_ERR(pd->log))
pd->log = NULL;
mutex_init(&pd->lock);
device_initialize(&pd->dev);
pd->dev.parent = parent;
pd->dev.release = usbpd_release;
dev_set_drvdata(&pd->dev, pd);
ret = dev_set_name(&pd->dev, "usbpd%d", num_pd_instances++);
if (ret < 0)
goto free_buffer;
ret = device_add(&pd->dev);
if (ret < 0)
goto free_buffer;
pd->wlc_supported = device_property_read_bool(parent,
"goog,wlc-supported");
#ifdef CONFIG_DEBUG_FS
ret = pd_engine_debugfs_init(pd);
if (ret < 0)
goto del_pd;
#endif
parse_cmdline();
device_init_wakeup(&pd->dev, true);
pd->vbus = devm_regulator_get(parent, "vbus");
if (IS_ERR_OR_NULL(pd->vbus)) {
dev_err(&pd->dev, "Can't find vbus-supply\n");
ret = PTR_ERR(pd->vbus);
goto exit_debugfs;
}
pd->vconn = devm_regulator_get(parent, "vconn");
if (IS_ERR_OR_NULL(pd->vconn)) {
dev_err(&pd->dev, "Can't find vconn-supply\n");
ret = PTR_ERR(pd->vconn);
goto exit_debugfs;
}
pd->ext_vbus_supported = device_property_read_bool(parent,
"google,ext_vbus-supported");
if (pd->ext_vbus_supported) {
pd->ext_vbus = devm_regulator_get(parent, "ext-vbus");
if (IS_ERR_OR_NULL(pd->ext_vbus)) {
dev_err(&pd->dev, "Can't find ext-vbus-supply\n");
ret = PTR_ERR(pd->ext_vbus);
goto exit_debugfs;
}
pd->hw_reg_nb.notifier_call = ext_boost_changed;
ret = regulator_register_notifier(pd->ext_vbus, &pd->hw_reg_nb);
if (ret) {
dev_err(&pd->dev, "Couldn't register ext_boost notifier\n");
goto exit_debugfs;
}
}
pd->switch_based_on_maxpower = device_property_read_bool(parent,
"google,maxpower-switch");
pd->fixed_pdo_5V3A = device_property_read_bool(parent,
"google,fixed_pdo_5V3A");
pd->extcon = devm_extcon_dev_allocate(parent, usbpd_extcon_cable);
if (IS_ERR(pd->extcon)) {
dev_err(&pd->dev, "extcon allocation failed\n");
ret = PTR_ERR(pd->extcon);
goto unreg_hw_reg_nb;
}
ret = devm_extcon_dev_register(parent, pd->extcon);
if (ret < 0) {
dev_err(&pd->dev, "failed to register extcon device");
goto unreg_hw_reg_nb;
}
/* Support reporting polarity and speed via properties */
extcon_set_property_capability(pd->extcon, EXTCON_USB,
EXTCON_PROP_USB_TYPEC_POLARITY);
extcon_set_property_capability(pd->extcon, EXTCON_USB,
EXTCON_PROP_USB_SS);
extcon_set_property_capability(pd->extcon, EXTCON_USB,
EXTCON_PROP_USB_TYPEC_MED_HIGH_CURRENT);
extcon_set_property_capability(pd->extcon, EXTCON_USB_HOST,
EXTCON_PROP_USB_TYPEC_POLARITY);
extcon_set_property_capability(pd->extcon, EXTCON_USB_HOST,
EXTCON_PROP_USB_SS);
pd->wq = create_singlethread_workqueue(dev_name(&pd->dev));
if (!pd->wq) {
dev_err(&pd->dev, "workqueue creation failed\n");
ret = -ENOMEM;
goto unreg_hw_reg_nb;
}
pd->default_src_cap = true;
INIT_DELAYED_WORK(&pd->ext_vbus_work, update_external_vbus);
INIT_WORK(&pd->update_pdo_work, update_src_caps);
pd->pd_disabled = false;
pd->usb_psy = power_supply_get_by_name("usb");
if (!pd->usb_psy) {
dev_err(&pd->dev,
"Could not get USB power_supply, deferring probe");
ret = -EPROBE_DEFER;
goto del_wq;
}
if (pd->wlc_supported) {
pd->wireless_psy = power_supply_get_by_name("wireless");
if (!pd->wireless_psy) {
logbuffer_log(pd->log,
"Could not get wireless power_supply, deferring probe");
ret = -EPROBE_DEFER;
goto put_psy_usb;
}
}
pd->usb_icl_votable = find_votable("USB_ICL");
if (pd->usb_icl_votable == NULL) {
logbuffer_log(pd->log,
"Couldn't find USB_ICL votable, deferring probe");
ret = -EPROBE_DEFER;
goto put_psy_wireless;
}
pd->apsd_disable_votable = find_votable("APSD_DISABLE");
if (pd->apsd_disable_votable == NULL) {
logbuffer_log(pd->log,
"Couldn't find APSD_DISABLE votable, deferring probe"
);
ret = -EPROBE_DEFER;
goto put_psy_wireless;
}
pd->disable_power_role_switch =
find_votable("DISABLE_POWER_ROLE_SWITCH");
if (pd->disable_power_role_switch == NULL) {
dev_err(&pd->dev,
"Couldn't find DISABLE_POWER_ROLE_SWITCH votable, deferring probe");
ret = -EPROBE_DEFER;
goto put_psy_wireless;
}
pd->src_cap_votable = create_votable("SWITCH_SRC_CAP", VOTE_MIN,
pd_switch_src_cap_callback,
pd);
if (IS_ERR(pd->src_cap_votable)) {
ret = PTR_ERR(pd->src_cap_votable);
dev_err(&pd->dev, "Couldn't create SWITCH_SRC_CAP votable (%d), deferring probe",
ret);
ret = -EPROBE_DEFER;
goto put_psy_wireless;
}
/* initialize votable */
vote(pd->usb_icl_votable, OTG_ICL_VOTER, false, 0);
vote(pd->apsd_disable_votable, OTG_DISABLE_APSD_VOTER, false, 0);
vote(pd->disable_power_role_switch, TCPM_DISABLE_CC_VOTER, false, 0);
vote(pd->usb_icl_votable, LIMIT_USB_VOTER, false, 0);
vote(pd->src_cap_votable, DEFAULT_SRC_CAP_VOTER, true, 900);
vote(pd->src_cap_votable, WIRELESS_SRC_CAP_VOTER, false, 0);
vote(pd->src_cap_votable, LIMIT_SRC_CAP_VOTER, false, 0);
pd->ext_vbus_nb.notifier_call = update_ext_vbus;
ext_vbus_register_notify(&pd->ext_vbus_nb);
/*
* TCPM callbacks may access pd->usb_psy. Therefore, tcpm_register_port
* must be called after pd->usb_psy is initialized.
*/
ret = init_tcpc_dev(pd, parent);
if (ret < 0)
goto unreg_ext_vbus;
pd->tcpm_port = tcpm_register_port(&pd->dev, &pd->tcpc_dev);
if (IS_ERR(pd->tcpm_port)) {
dev_err(&pd->dev, "tcpm port register failed\n");
ret = PTR_ERR(pd->tcpm_port);
goto unreg_ext_vbus;
}
pd->psy_nb.notifier_call = psy_changed;
ret = power_supply_reg_notifier(&pd->psy_nb);
if (ret < 0) {
dev_err(&pd->dev, "psy notifier register failed\n");
goto unreg_tcpm;
}
init_pd_phy_params(&pd->pdphy_params);
pd_phy_assign_pm_callbacks(&pd->pdphy_params);
val.intval = POWER_SUPPLY_PD_INACTIVE;
ret = power_supply_set_property(pd->usb_psy,
POWER_SUPPLY_PROP_PD_ACTIVE,
&val);
if (ret < 0)
dev_err(&pd->dev, "Cannot set POWER_SUPPLY_PROP_PD_ACTIVE\n");
psy_changed(&pd->psy_nb, PSY_EVENT_PROP_CHANGED, pd->usb_psy);
pd->suspend_supported = true;
for (i = 0; usbpd_device_attrs[i]; i++) {
ret = device_create_file(&pd->dev, usbpd_device_attrs[i]);
if (ret < 0)
dev_err(&pd->dev,
"Unable to create device attr[%d] ret:%d:",
i, ret);
}
return pd;
unreg_tcpm:
tcpm_unregister_port(pd->tcpm_port);
unreg_ext_vbus:
ext_vbus_unregister_notify(&pd->ext_vbus_nb);
destroy_votable(pd->src_cap_votable);
put_psy_wireless:
if (pd->wlc_supported)
power_supply_put(pd->wireless_psy);
put_psy_usb:
power_supply_put(pd->usb_psy);
del_wq:
destroy_workqueue(pd->wq);
unreg_hw_reg_nb:
if (pd->hw_reg_nb.notifier_call) {
regulator_unregister_notifier(pd->ext_vbus, &pd->hw_reg_nb);
pd->hw_reg_nb.notifier_call = NULL;
}
exit_debugfs:
#ifdef CONFIG_DEBUG_FS
pd_engine_debugfs_exit(pd);
del_pd:
#endif
device_del(&pd->dev);
free_buffer:
if (pd->log)
logbuffer_unregister(pd->log);
num_pd_instances--;
put_device(&pd->dev);
return ERR_PTR(ret);
}
EXPORT_SYMBOL(usbpd_create);
/**
* usbpd_destroy - Removes and frees a usbpd instance
* @pd: the instance to destroy
*
* Derived from policy_engine.c.
*/
void usbpd_destroy(struct usbpd *pd)
{
if (!pd)
return;
power_supply_unreg_notifier(&pd->psy_nb);
tcpm_unregister_port(pd->tcpm_port);
ext_vbus_unregister_notify(&pd->ext_vbus_nb);
destroy_votable(pd->src_cap_votable);
if (pd->wlc_supported)
power_supply_put(pd->wireless_psy);
power_supply_put(pd->usb_psy);
destroy_workqueue(pd->wq);
if (pd->hw_reg_nb.notifier_call) {
regulator_unregister_notifier(pd->ext_vbus, &pd->hw_reg_nb);
pd->hw_reg_nb.notifier_call = NULL;
}
#ifdef CONFIG_DEBUG_FS
pd_engine_debugfs_exit(pd);
#endif
if (pd->log)
logbuffer_unregister(pd->log);
num_pd_instances--;
device_unregister(&pd->dev);
}
EXPORT_SYMBOL(usbpd_destroy);
static int __init pd_engine_init(void)
{
#if defined(CONFIG_QPNP_USB_PDPHY_MODULE)
return pdphy_driver_init();
#else
return 0;
#endif
}
module_init(pd_engine_init);
static void __exit pd_engine_exit(void)
{
#if defined(CONFIG_QPNP_USB_PDPHY_MODULE)
pdphy_driver_exit();
#endif
}
module_exit(pd_engine_exit);
MODULE_DESCRIPTION("USB PD Engine based on Type-C Port Manager");
MODULE_LICENSE("GPL v2");