blob: ba1fadb9c5066d446733fa0affcc50a1d391cb22 [file] [log] [blame]
/*
* 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 <linux/delay.h>
#include <linux/extcon.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/power_supply.h>
#include <linux/property.h>
#include <linux/regulator/consumer.h>
#include <linux/seq_file.h>
#include <linux/slab.h>
#include <linux/types.h>
#include <linux/usb/typec.h>
#include <linux/workqueue.h>
#include "../typec/pd.h"
#include "../typec/tcpm.h"
#include "usbpd.h"
#define LOG_BUFFER_ENTRIES 1024
#define LOG_BUFFER_ENTRY_SIZE 256
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 notifier_block psy_nb;
struct regulator *vbus;
struct regulator *vconn;
bool vbus_output;
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;
bool extcon_usb_ss;
struct mutex lock; /* struct usbpd access lock */
struct workqueue_struct *wq;
/* debugfs logging */
struct dentry *dentry;
struct mutex logbuffer_lock; /* log buffer access lock */
int logbuffer_head;
int logbuffer_tail;
u8 *logbuffer[LOG_BUFFER_ENTRIES];
bool in_pr_swap;
bool suspend_supported;
};
/*
* Logging
*/
static bool pd_engine_log_full(struct usbpd *pd)
{
return pd->logbuffer_tail ==
(pd->logbuffer_head + 1) % LOG_BUFFER_ENTRIES;
}
static void _pd_engine_log(struct usbpd *pd, const char *fmt, va_list args)
{
char tmpbuffer[LOG_BUFFER_ENTRY_SIZE];
u64 ts_nsec = local_clock();
unsigned long rem_nsec;
if (!pd->logbuffer[pd->logbuffer_head]) {
pd->logbuffer[pd->logbuffer_head] =
kzalloc(LOG_BUFFER_ENTRY_SIZE, GFP_KERNEL);
if (!pd->logbuffer[pd->logbuffer_head])
return;
}
vsnprintf(tmpbuffer, sizeof(tmpbuffer), fmt, args);
mutex_lock(&pd->logbuffer_lock);
if (pd_engine_log_full(pd)) {
pd->logbuffer_head = max(pd->logbuffer_head - 1, 0);
strlcpy(tmpbuffer, "overflow", LOG_BUFFER_ENTRY_SIZE);
}
if (pd->logbuffer_head < 0 ||
pd->logbuffer_head >= LOG_BUFFER_ENTRIES) {
dev_warn(&pd->dev,
"Bad log buffer index %d\n", pd->logbuffer_head);
goto abort;
}
if (!pd->logbuffer[pd->logbuffer_head]) {
dev_warn(&pd->dev,
"Log buffer index %d is NULL\n", pd->logbuffer_head);
goto abort;
}
rem_nsec = do_div(ts_nsec, 1000000000);
scnprintf(pd->logbuffer[pd->logbuffer_head],
LOG_BUFFER_ENTRY_SIZE, "[%5lu.%06lu] %s",
(unsigned long)ts_nsec, rem_nsec / 1000,
tmpbuffer);
pd->logbuffer_head = (pd->logbuffer_head + 1) % LOG_BUFFER_ENTRIES;
abort:
mutex_unlock(&pd->logbuffer_lock);
}
static void pd_engine_log(struct usbpd *pd, const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
_pd_engine_log(pd, fmt, args);
va_end(args);
}
static int pd_engine_seq_show(struct seq_file *s, void *v)
{
struct usbpd *pd = (struct usbpd *)s->private;
int tail;
mutex_lock(&pd->logbuffer_lock);
tail = pd->logbuffer_tail;
while (tail != pd->logbuffer_head) {
seq_printf(s, "%s\n", pd->logbuffer[tail]);
tail = (tail + 1) % LOG_BUFFER_ENTRIES;
}
if (!seq_has_overflowed(s))
pd->logbuffer_tail = tail;
mutex_unlock(&pd->logbuffer_lock);
return 0;
}
static int pd_engine_debug_open(struct inode *inode, struct file *file)
{
return single_open(file, pd_engine_seq_show, inode->i_private);
}
static const struct file_operations pd_engine_debug_operations = {
.open = pd_engine_debug_open,
.llseek = seq_lseek,
.read = seq_read,
.release = single_release,
};
static struct dentry *rootdir;
static int pd_engine_debugfs_init(struct usbpd *pd)
{
mutex_init(&pd->logbuffer_lock);
/* /sys/kernel/debug/pd_engine/usbpdX */
if (!rootdir) {
rootdir = debugfs_create_dir("pd_engine", NULL);
if (!rootdir)
return -ENOMEM;
}
pd->dentry = debugfs_create_file(dev_name(&pd->dev),
S_IFREG | S_IRUGO, rootdir,
pd, &pd_engine_debug_operations);
return 0;
}
static void pd_engine_debugfs_exit(struct usbpd *pd)
{
debugfs_remove(pd->dentry);
}
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_NON_COMPLIANT:
return "NON_COMPLIANT";
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";
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 "Rd-def";
case TYPEC_CC_RP_1_5:
return "Rd-1.5";
case TYPEC_CC_RP_3_0:
return "Rd-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;
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_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_usb_data_role(struct usbpd *pd)
{
int ret;
bool extcon_usb;
ret = extcon_set_cable_state_(pd->extcon, EXTCON_USB_CC,
pd->extcon_usb_cc);
if (ret < 0) {
pd_engine_log(pd, "unable to set extcon usb cc [%s], ret=%d",
pd->extcon_usb_cc ? "Y" : "N", ret);
return ret;
}
ret = extcon_set_cable_state_(pd->extcon, EXTCON_USB_SPEED,
pd->extcon_usb_ss);
if (ret < 0) {
pd_engine_log(pd, "unable to set extcon usb ss [%s], ret=%d",
pd->extcon_usb_ss ? "Y" : "N", ret);
return ret;
}
/* 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);
/*
* 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_cable_state_(pd->extcon, EXTCON_USB_HOST,
pd->extcon_usb_host);
if (ret < 0) {
pd_engine_log(pd,
"unable to set extcon usb host [%s], ret=%d",
pd->extcon_usb_host ? "Y" : "N", ret);
return ret;
}
ret = extcon_set_cable_state_(pd->extcon, EXTCON_USB,
extcon_usb);
if (ret < 0) {
pd_engine_log(pd,
"unable to set extcon usb [%s], ret=%d",
extcon_usb ? "Y" : "N", ret);
return ret;
}
} else {
ret = extcon_set_cable_state_(pd->extcon, EXTCON_USB,
extcon_usb);
if (ret < 0) {
pd_engine_log(pd,
"unable to set extcon usb [%s], ret=%d",
extcon_usb ? "Y" : "N", ret);
return ret;
}
ret = extcon_set_cable_state_(pd->extcon, EXTCON_USB_HOST,
pd->extcon_usb_host);
if (ret < 0) {
pd_engine_log(pd,
"unable to set extcon usb host [%s], ret=%d",
pd->extcon_usb_host ? "Y" : "N", ret);
return ret;
}
}
pd_engine_log(pd,
"usb extcon: cc [%s], speed [%s], host [%s], usb [%s]",
pd->extcon_usb_cc ? "Y" : "N",
pd->extcon_usb_ss ? "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 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 apsd_done;
union power_supply_propval val;
int ret = 0;
ret = power_supply_get_property(pd->usb_psy,
POWER_SUPPLY_PROP_TYPE, &val);
if (ret < 0) {
pd_engine_log(pd, "Unable to read TYPE, ret=%d", ret);
return;
}
psy_type = val.intval;
ret = power_supply_get_property(pd->usb_psy,
POWER_SUPPLY_PROP_PD_APSD_DONE, &val);
if (ret < 0) {
pd_engine_log(pd, "Unable to read APSD_DONE, ret=%d",
ret);
return;
}
apsd_done = !!val.intval;
ret = power_supply_get_property(pd->usb_psy,
POWER_SUPPLY_PROP_PRESENT, &val);
if (ret < 0) {
pd_engine_log(pd, "Unable to read PRESENT, ret=%d",
ret);
return;
}
vbus_present = val.intval;
ret = power_supply_get_property(pd->usb_psy,
POWER_SUPPLY_PROP_TYPEC_MODE, &val);
if (ret < 0) {
pd_engine_log(pd, "Unable to read TYPEC_MODE, ret=%d",
ret);
return;
}
typec_mode = val.intval;
ret = power_supply_get_property(pd->usb_psy,
POWER_SUPPLY_PROP_TYPEC_CC_ORIENTATION,
&val);
if (ret < 0) {
pd_engine_log(pd,
"Unable to read TYPEC_CC_ORIENTATION, ret=%d",
ret);
return;
}
typec_cc_orientation = val.intval;
parse_cc_status(typec_mode, typec_cc_orientation, &cc1, &cc2);
pd_engine_log(pd,
"type [%s], apsd done [%s], vbus present [%s], typec_mode [%s], typec_orientation [%s], cc1 [%s], cc2 [%s]",
get_psy_type_name(psy_type),
apsd_done ? "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));
mutex_lock(&pd->lock);
pd->psy_type = 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) {
pd_engine_log(pd, "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 (cc1 != pd->cc1 || cc2 != pd->cc2) {
pd_engine_log(pd, "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));
pd->cc1 = cc1;
pd->cc2 = cc2;
tcpm_cc_change(pd->tcpm_port);
}
if (apsd_done && pd->pending_update_usb_data) {
pd_engine_log(pd,
"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:
kfree(event);
mutex_unlock(&pd->lock);
}
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->usb_psy || evt != PSY_EVENT_PROP_CHANGED)
return 0;
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;
pd_engine_log(pd, "tcpm_get_vbus: %s",
vbus_on ? "True" : "False");
ret = vbus_on ? 1 : 0;
mutex_unlock(&pd->lock);
return ret;
}
static int tcpm_set_cc(struct tcpc_dev *dev, enum typec_cc_status cc)
{
union power_supply_propval 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:
val.intval = POWER_SUPPLY_TYPEC_PR_NONE;
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;
break;
case TYPEC_CC_RP_1_5:
val.intval = POWER_SUPPLY_TYPEC_PR_SOURCE_1_5;
break;
default:
pd_engine_log(pd, "tcpm_set_cc: invalid cc %s",
get_typec_cc_status_name(cc));
ret = -EINVAL;
goto unlock;
}
ret = power_supply_set_property(pd->usb_psy,
POWER_SUPPLY_PROP_TYPEC_POWER_ROLE,
&val);
if (ret < 0)
pd_engine_log(pd, "unable to set POWER_ROLE, ret=%d",
ret);
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);
pd_engine_log(pd, "tcpm_get_cc: cc1=%s, cc2=%s",
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) {
pd_engine_log(pd, "unable to turn %s vconn, ret=%d",
on ? "on" : "off", ret);
goto unlock;
}
pd->vconn_output = on;
}
pd_engine_log(pd, "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;
mutex_lock(&pd->lock);
if (on != pd->vbus_output) {
if (on)
ret = regulator_enable(pd->vbus);
else
ret = regulator_disable(pd->vbus);
if (ret < 0) {
pd_engine_log(pd, "unable to turn %s vbus, ret=%d",
on ? "on" : "off", ret);
goto unlock;
}
pd->vbus_output = on;
/*
* No interrupt will be trigerred for the vbus change.
* Manually inform tcpm to check the vbus change.
*/
tcpm_vbus_change(pd->tcpm_port);
}
pd_engine_log(pd, "set vbus: %s", on ? "on" : "off");
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;
val.intval = mv * 1000;
ret = power_supply_set_property(pd->usb_psy,
POWER_SUPPLY_PROP_VOLTAGE_MAX,
&val);
if (ret < 0) {
pd_engine_log(pd, "unable to set 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) {
pd_engine_log(pd, "unable to set pd current max to %d, ret=%d",
max_ma, ret);
return ret;
}
pd_engine_log(pd, "max_ma := %d, mv := %d", max_ma, mv);
return ret;
}
static int tcpm_set_pd_rx(struct tcpc_dev *dev, bool on)
{
union power_supply_propval val = {0};
struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev);
int ret = 0;
if (pd->pdphy_open == on) {
pd_engine_log(pd, "pd_phy already %s",
on ? "open" : "closed");
return 0;
}
if (on) {
ret = pd_phy_open(&pd->pdphy_params);
if (ret < 0) {
pd_engine_log(pd, "unable to %s pd_phy, ret=%d",
on ? "open" : "close", ret);
return ret;
}
val.intval = 1;
} else {
/* pd_phy_close() has no return value. */
pd_phy_close();
val.intval = 0;
}
pd->pdphy_open = on;
pd_engine_log(pd, "%s pd_phy", on ? "open" : "close");
ret = power_supply_set_property(pd->usb_psy,
POWER_SUPPLY_PROP_PD_CC_OVERRIDE,
&val);
if (ret < 0) {
pd_engine_log(pd, "unable to set CC_OVERRIDE to %s, ret=%d",
on ? "true" : "false",
ret);
return ret;
}
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";
}
}
#define PD_TX_TIMEOUT_MS (PD_T_TCPC_TX_TIMEOUT - 10)
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, PD_TX_TIMEOUT_MS);
signal = true;
break;
case TCPC_TX_CABLE_RESET:
ret = pd_phy_signal(CABLE_RESET_SIG, PD_TX_TIMEOUT_MS);
signal = true;
break;
case TCPC_TX_SOP:
ret = pd_phy_write(msg->header, (u8 *)msg->payload,
get_data_len(msg->header),
SOP_MSG, PD_TX_TIMEOUT_MS);
signal = false;
break;
case TCPC_TX_SOP_PRIME:
ret = pd_phy_write(msg->header, (u8 *)msg->payload,
get_data_len(msg->header),
SOPI_MSG, PD_TX_TIMEOUT_MS);
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, PD_TX_TIMEOUT_MS);
signal = false;
break;
default:
pd_engine_log(pd, "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)
pd_engine_log(pd,
"pd tx type [%s], pdphy ret [%d], status [%s]",
get_tcpm_transmit_type_name(type),
ret, get_tcpm_transmit_status_name(status));
else
pd_engine_log(pd,
"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;
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:
pd_engine_log(pd, "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)
pd_engine_log(pd, "queue pd tx header [%#x], type [%s]",
msg->header, get_tcpm_transmit_type_name(type));
else
pd_engine_log(pd, "queue pd tx type [%s]",
get_tcpm_transmit_type_name(type));
return 0;
}
static int tcpm_start_drp_toggling(struct tcpc_dev *dev,
enum typec_cc_status cc)
{
/*
* Ignore the typec_cc_status for now. As current no
* src_pdo is configured, the default is Rp_def.
*/
union power_supply_propval val = {0};
struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev);
int ret;
mutex_lock(&pd->lock);
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) {
pd_engine_log(pd, "unable to set POWER_ROLE to PR_DUAL, ret=%d",
ret);
goto unlock;
}
pd->cc1 = TYPEC_CC_OPEN;
pd->cc2 = TYPEC_CC_OPEN;
pd_engine_log(pd, "start toggling");
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;
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) {
pd_engine_log(pd,
"unable to set POWER_ROLE to PR_DUAL, ret=%d",
ret);
goto unlock;
}
/* Required for the PMIC to recover */
pd_engine_log(pd, "sleeping for 20mec");
msleep(20);
}
val.intval = pr_swap ? 1 : 0;
ret = power_supply_set_property(pd->usb_psy,
POWER_SUPPLY_PROP_PR_SWAP,
&val);
if (ret < 0) {
pd_engine_log(pd,
"unable to set PR_SWAP to %d, ret=%d",
pr_swap ? 1 : 0, ret);
goto unlock;
}
pd->in_pr_swap = pr_swap;
pd_engine_log(pd, "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;
pd_engine_log(pd, "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) {
pd_engine_log(pd,
"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) {
pd_engine_log(pd, "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;
}
pd_engine_log(pd, "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)
{
int ret;
union power_supply_propval val = {0};
bool apsd_done;
pd->extcon_usb_cc = pd->is_cable_flipped;
pd->extcon_usb_ss = EXTCON_USB_SUPER_SPEED;
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;
}
ret = power_supply_get_property(pd->usb_psy,
POWER_SUPPLY_PROP_PD_APSD_DONE,
&val);
if (ret < 0) {
pd_engine_log(pd, "Unable to read APSD_DONE, ret=%d", ret);
return ret;
}
if (val.intval == 0)
apsd_done = false;
else
apsd_done = true;
pd_engine_log(pd,
"set usb_data_role: power [%s], data [%s], apsd_done [%s], attached [%s]",
get_typec_role_name(role),
get_typec_data_role_name(data),
apsd_done ? "Y" : "N",
attached ? "Y" : "N");
if (attached && role == TYPEC_SINK && !apsd_done) {
/* wait for APSD done */
pd_engine_log(pd,
"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)
{
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);
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);
pd_engine_log(pd, "received pd hard reset");
break;
default:
pd_engine_log(pd, "received unsupported signal: %d",
type);
return;
}
}
static void pd_phy_message_rx(struct usbpd *pd, enum pd_msg_type type,
u8 *buf, size_t len)
{
struct pd_message msg;
if (len < 2) {
pd_engine_log(pd, "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) {
pd_engine_log(pd, "header len %d != len %ld",
get_data_len(msg.header), len);
return;
}
if (len > PD_MAX_PAYLOAD * 4) {
pd_engine_log(pd, "len %ld > PD_MAX_PAYLOAD", len);
return;
}
memcpy(msg.payload, buf, len);
pd_engine_log(pd, "pd rx header [%#x]", msg.header);
tcpm_pd_receive(pd->tcpm_port, &msg);
}
static void pd_phy_shutdown(struct usbpd *pd)
{
int rc = 0;
mutex_lock(&pd->lock);
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;
}
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);
pd_engine_log(pd, "pd phy shutdown");
}
enum pdo_role {
SNK_PDO,
SRC_PDO,
};
static const char * const pdo_prop_name[] = {
[SNK_PDO] = "snk-pdo",
[SRC_PDO] = "src-pdo",
};
#define PDO_FIXED_FLAGS \
(PDO_FIXED_DUAL_ROLE | PDO_FIXED_DATA_SWAP | PDO_FIXED_USB_COMM)
/*
static const u32 src_pdo[] = {
PDO_FIXED(5000, 900, PDO_FIXED_FLAGS),
};
static const u32 snk_pdo[] = {
PDO_FIXED(5000, 3000, PDO_FIXED_FLAGS),
PDO_FIXED(9000, 3000, PDO_FIXED_FLAGS),
};
static const struct tcpc_config pd_tcpc_config = {
.src_pdo = src_pdo,
.nr_src_pdo = ARRAY_SIZE(src_pdo),
.snk_pdo = snk_pdo,
.nr_snk_pdo = ARRAY_SIZE(snk_pdo),
.max_snk_mv = 9000,
.max_snk_ma = 3000,
.max_snk_mw = 27000,
.operating_snk_mw = 7600,
.type = TYPEC_PORT_DRP,
.default_role = TYPEC_SINK,
.try_role_hw = true,
.alt_modes = NULL,
};
*/
static u32 *parse_pdo(struct usbpd *pd, enum pdo_role role,
unsigned int *nr_pdo)
{
struct device *dev = &pd->dev;
u32 *dt_array;
u32 *pdo;
int i, count, rc;
count = device_property_read_u32_array(dev->parent, pdo_prop_name[role],
NULL, 0);
if (count > 0) {
if (count % 4)
return ERR_PTR(-EINVAL);
*nr_pdo = count / 4;
dt_array = devm_kcalloc(dev, count, sizeof(*dt_array),
GFP_KERNEL);
if (!dt_array)
return ERR_PTR(-ENOMEM);
rc = device_property_read_u32_array(dev->parent,
pdo_prop_name[role],
dt_array, count);
if (rc)
return ERR_PTR(rc);
pdo = devm_kcalloc(dev, *nr_pdo, sizeof(*pdo), GFP_KERNEL);
if (!pdo)
return ERR_PTR(-ENOMEM);
for (i = 0; i < *nr_pdo; i++) {
switch (dt_array[i * 4]) {
case PDO_TYPE_FIXED:
pdo[i] = PDO_FIXED(dt_array[i * 4 + 1],
dt_array[i * 4 + 2],
PDO_FIXED_FLAGS);
break;
case PDO_TYPE_BATT:
pdo[i] = PDO_BATT(dt_array[i * 4 + 1],
dt_array[i * 4 + 2],
dt_array[i * 4 + 3]);
break;
case PDO_TYPE_VAR:
pdo[i] = PDO_VAR(dt_array[i * 4 + 1],
dt_array[i * 4 + 2],
dt_array[i * 4 + 3]);
break;
/*case PDO_TYPE_AUG:*/
default:
return ERR_PTR(-EINVAL);
}
}
return pdo;
}
return ERR_PTR(-EINVAL);
}
static int init_tcpc_config(struct tcpc_dev *pd_tcpc_dev)
{
struct usbpd *pd = container_of(pd_tcpc_dev, struct usbpd, tcpc_dev);
struct device *dev = &pd->dev;
struct tcpc_config *config;
int ret;
pd_tcpc_dev->config = devm_kzalloc(dev, sizeof(*config), GFP_KERNEL);
if (!pd_tcpc_dev->config)
return -ENOMEM;
config = pd_tcpc_dev->config;
ret = device_property_read_u32(dev->parent, "port-type", &config->type);
if (ret < 0)
return ret;
switch (config->type) {
case TYPEC_PORT_UFP:
config->snk_pdo = parse_pdo(pd, SNK_PDO, &config->nr_snk_pdo);
if (IS_ERR(config->snk_pdo))
return PTR_ERR(config->snk_pdo);
break;
case TYPEC_PORT_DFP:
config->src_pdo = parse_pdo(pd, SRC_PDO, &config->nr_src_pdo);
if (IS_ERR(config->src_pdo))
return PTR_ERR(config->src_pdo);
break;
case TYPEC_PORT_DRP:
config->snk_pdo = parse_pdo(pd, SNK_PDO, &config->nr_snk_pdo);
if (IS_ERR(config->snk_pdo))
return PTR_ERR(config->snk_pdo);
config->src_pdo = parse_pdo(pd, SRC_PDO, &config->nr_src_pdo);
if (IS_ERR(config->src_pdo))
return PTR_ERR(config->src_pdo);
ret = device_property_read_u32(dev->parent, "default-role",
&config->default_role);
if (ret < 0)
return ret;
config->try_role_hw = device_property_read_bool(dev->parent,
"try-role-hw");
break;
default:
return -EINVAL;
}
if (config->type == TYPEC_PORT_UFP || config->type == TYPEC_PORT_DRP) {
ret = device_property_read_u32(dev->parent, "max-snk-mv",
&config->max_snk_mv);
ret = device_property_read_u32(dev->parent, "max-snk-ma",
&config->max_snk_ma);
ret = device_property_read_u32(dev->parent, "max-snk-mw",
&config->max_snk_mw);
ret = device_property_read_u32(dev->parent, "op-snk-mw",
&config->operating_snk_mw);
if (ret < 0)
return ret;
}
/* TODO: parse alt mode from DT */
config->alt_modes = NULL;
config->self_powered = true;
return 0;
}
static int init_tcpc_dev(struct tcpc_dev *pd_tcpc_dev)
{
int ret;
ret = init_tcpc_config(pd_tcpc_dev);
if (ret < 0)
return ret;
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_drp_toggling = tcpm_start_drp_toggling;
pd_tcpc_dev->set_in_pr_swap = tcpm_set_in_pr_swap;
pd_tcpc_dev->set_suspend_supported = tcpm_set_suspend_supported;
pd_tcpc_dev->mux = NULL;
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 const unsigned int usbpd_extcon_cable[] = {
EXTCON_USB,
EXTCON_USB_HOST,
EXTCON_USB_CC,
EXTCON_USB_SPEED,
EXTCON_NONE,
};
/* EXTCON_USB and EXTCON_USB_HOST are mutually exclusive */
static const u32 usbpd_extcon_exclusive[] = {0x3, 0};
static int num_pd_instances;
/**
* 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;
struct usbpd *pd;
pd = kzalloc(sizeof(*pd), GFP_KERNEL);
if (!pd)
return ERR_PTR(-ENOMEM);
mutex_init(&pd->lock);
device_initialize(&pd->dev);
pd->dev.parent = parent;
dev_set_drvdata(&pd->dev, pd);
ret = dev_set_name(&pd->dev, "usbpd%d", num_pd_instances++);
if (ret < 0)
goto free_pd;
ret = device_add(&pd->dev);
if (ret < 0)
goto free_pd;
ret = pd_engine_debugfs_init(pd);
if (ret < 0)
goto del_pd;
pd->vbus = devm_regulator_get(parent, "vbus");
if (IS_ERR(pd->vbus)) {
ret = PTR_ERR(pd->vbus);
goto exit_debugfs;
}
pd->vconn = devm_regulator_get(parent, "vconn");
if (IS_ERR(pd->vconn)) {
ret = PTR_ERR(pd->vconn);
goto exit_debugfs;
}
pd->extcon = devm_extcon_dev_allocate(parent, usbpd_extcon_cable);
if (IS_ERR(pd->extcon)) {
pd_engine_log(pd, "failed to allocate extcon device");
ret = PTR_ERR(pd->extcon);
goto exit_debugfs;
}
pd->extcon->mutually_exclusive = usbpd_extcon_exclusive;
ret = devm_extcon_dev_register(parent, pd->extcon);
if (ret < 0) {
pd_engine_log(pd, "failed to register extcon device");
goto exit_debugfs;
}
pd->wq = create_singlethread_workqueue(dev_name(&pd->dev));
if (!pd->wq) {
ret = -ENOMEM;
goto exit_debugfs;
}
pd->usb_psy = power_supply_get_by_name("usb");
if (!pd->usb_psy) {
pd_engine_log(pd,
"Could not get USB power_supply, deferring probe");
ret = -EPROBE_DEFER;
goto del_wq;
}
/*
* 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->tcpc_dev);
if (ret < 0)
goto put_psy;
pd->tcpm_port = tcpm_register_port(&pd->dev, &pd->tcpc_dev);
if (IS_ERR(pd->tcpm_port)) {
ret = PTR_ERR(pd->tcpm_port);
goto put_psy;
}
psy_changed(&pd->psy_nb, PSY_EVENT_PROP_CHANGED, pd->usb_psy);
pd->psy_nb.notifier_call = psy_changed;
ret = power_supply_reg_notifier(&pd->psy_nb);
if (ret < 0)
goto unreg_tcpm;
init_pd_phy_params(&pd->pdphy_params);
pd->suspend_supported = true;
return pd;
unreg_tcpm:
tcpm_unregister_port(pd->tcpm_port);
put_psy:
power_supply_put(pd->usb_psy);
del_wq:
destroy_workqueue(pd->wq);
exit_debugfs:
pd_engine_debugfs_exit(pd);
del_pd:
device_del(&pd->dev);
free_pd:
num_pd_instances--;
kfree(pd);
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);
power_supply_put(pd->usb_psy);
destroy_workqueue(pd->wq);
pd_engine_debugfs_exit(pd);
device_del(&pd->dev);
num_pd_instances--;
kfree(pd);
}
EXPORT_SYMBOL(usbpd_destroy);
static int __init pd_engine_init(void)
{
return 0;
}
module_init(pd_engine_init);
static void __exit pd_engine_exit(void) {}
module_exit(pd_engine_exit);
MODULE_DESCRIPTION("USB PD Engine based on Type-C Port Manager");
MODULE_LICENSE("GPL v2");