diff mbox

[v7,1/6] typec: tcpm: Add core support for sink side PPS

Message ID daf72dd237653e55c0485e930108c19a4996aa4f.1521799705.git.Adam.Thomson.Opensource@diasemi.com (mailing list archive)
State Not Applicable, archived
Headers show

Commit Message

Adam Thomson March 23, 2018, 10:12 a.m. UTC
This commit adds code to handle requesting of PPS APDOs. Switching
between standard PDOs and APDOs, and re-requesting an APDO to
modify operating voltage/current will be triggered by an
external call into TCPM.

Signed-off-by: Adam Thomson <Adam.Thomson.Opensource@diasemi.com>
Acked-by: Heikki Krogerus <heikki.krogerus@linux.intel.com>
---
 drivers/usb/typec/tcpm.c | 517 ++++++++++++++++++++++++++++++++++++++++++++++-
 include/linux/usb/pd.h   |   4 +-
 include/linux/usb/tcpm.h |   1 +
 3 files changed, 509 insertions(+), 13 deletions(-)

Comments

Guenter Roeck March 26, 2018, 3:10 a.m. UTC | #1
On 03/23/2018 03:12 AM, Adam Thomson wrote:
> This commit adds code to handle requesting of PPS APDOs. Switching
> between standard PDOs and APDOs, and re-requesting an APDO to
> modify operating voltage/current will be triggered by an
> external call into TCPM.
> 
> Signed-off-by: Adam Thomson <Adam.Thomson.Opensource@diasemi.com>
> Acked-by: Heikki Krogerus <heikki.krogerus@linux.intel.com>

Reviewed-by: Guenter Roeck <linux@roeck-us.net>

> ---
>   drivers/usb/typec/tcpm.c | 517 ++++++++++++++++++++++++++++++++++++++++++++++-
>   include/linux/usb/pd.h   |   4 +-
>   include/linux/usb/tcpm.h |   1 +
>   3 files changed, 509 insertions(+), 13 deletions(-)
> 
> diff --git a/drivers/usb/typec/tcpm.c b/drivers/usb/typec/tcpm.c
> index 4c0fc54..1a66c9e 100644
> --- a/drivers/usb/typec/tcpm.c
> +++ b/drivers/usb/typec/tcpm.c
> @@ -47,6 +47,7 @@
>   	S(SNK_DISCOVERY_DEBOUNCE_DONE),		\
>   	S(SNK_WAIT_CAPABILITIES),		\
>   	S(SNK_NEGOTIATE_CAPABILITIES),		\
> +	S(SNK_NEGOTIATE_PPS_CAPABILITIES),	\
>   	S(SNK_TRANSITION_SINK),			\
>   	S(SNK_TRANSITION_SINK_VBUS),		\
>   	S(SNK_READY),				\
> @@ -166,6 +167,16 @@ struct pd_mode_data {
>   	struct typec_altmode_desc altmode_desc[SVID_DISCOVERY_MAX];
>   };
>   
> +struct pd_pps_data {
> +	u32 min_volt;
> +	u32 max_volt;
> +	u32 max_curr;
> +	u32 out_volt;
> +	u32 op_curr;
> +	bool supported;
> +	bool active;
> +};
> +
>   struct tcpm_port {
>   	struct device *dev;
>   
> @@ -233,6 +244,7 @@ struct tcpm_port {
>   	struct completion swap_complete;
>   	int swap_status;
>   
> +	unsigned int negotiated_rev;
>   	unsigned int message_id;
>   	unsigned int caps_count;
>   	unsigned int hard_reset_count;
> @@ -259,6 +271,7 @@ struct tcpm_port {
>   	unsigned int max_snk_ma;
>   	unsigned int max_snk_mw;
>   	unsigned int operating_snk_mw;
> +	bool update_sink_caps;
>   
>   	/* Requested current / voltage */
>   	u32 current_limit;
> @@ -275,8 +288,13 @@ struct tcpm_port {
>   	/* VDO to retry if UFP responder replied busy */
>   	u32 vdo_retry;
>   
> -	/* Alternate mode data */
> +	/* PPS */
> +	struct pd_pps_data pps_data;
> +	struct completion pps_complete;
> +	bool pps_pending;
> +	int pps_status;
>   
> +	/* Alternate mode data */
>   	struct pd_mode_data mode_data;
>   	struct typec_altmode *partner_altmode[SVID_DISCOVERY_MAX];
>   	struct typec_altmode *port_altmode[SVID_DISCOVERY_MAX];
> @@ -494,6 +512,16 @@ static void tcpm_log_source_caps(struct tcpm_port *port)
>   				  pdo_max_voltage(pdo),
>   				  pdo_max_power(pdo));
>   			break;
> +		case PDO_TYPE_APDO:
> +			if (pdo_apdo_type(pdo) == APDO_TYPE_PPS)
> +				scnprintf(msg, sizeof(msg),
> +					  "%u-%u mV, %u mA",
> +					  pdo_pps_apdo_min_voltage(pdo),
> +					  pdo_pps_apdo_max_voltage(pdo),
> +					  pdo_pps_apdo_max_current(pdo));
> +			else
> +				strcpy(msg, "undefined APDO");
> +			break;
>   		default:
>   			strcpy(msg, "undefined");
>   			break;
> @@ -777,11 +805,13 @@ static int tcpm_pd_send_source_caps(struct tcpm_port *port)
>   		msg.header = PD_HEADER_LE(PD_CTRL_REJECT,
>   					  port->pwr_role,
>   					  port->data_role,
> +					  port->negotiated_rev,
>   					  port->message_id, 0);
>   	} else {
>   		msg.header = PD_HEADER_LE(PD_DATA_SOURCE_CAP,
>   					  port->pwr_role,
>   					  port->data_role,
> +					  port->negotiated_rev,
>   					  port->message_id,
>   					  port->nr_src_pdo);
>   	}
> @@ -802,11 +832,13 @@ static int tcpm_pd_send_sink_caps(struct tcpm_port *port)
>   		msg.header = PD_HEADER_LE(PD_CTRL_REJECT,
>   					  port->pwr_role,
>   					  port->data_role,
> +					  port->negotiated_rev,
>   					  port->message_id, 0);
>   	} else {
>   		msg.header = PD_HEADER_LE(PD_DATA_SINK_CAP,
>   					  port->pwr_role,
>   					  port->data_role,
> +					  port->negotiated_rev,
>   					  port->message_id,
>   					  port->nr_snk_pdo);
>   	}
> @@ -1173,6 +1205,7 @@ static void vdm_run_state_machine(struct tcpm_port *port)
>   		msg.header = PD_HEADER_LE(PD_DATA_VENDOR_DEF,
>   					  port->pwr_role,
>   					  port->data_role,
> +					  port->negotiated_rev,
>   					  port->message_id, port->vdo_count);
>   		for (i = 0; i < port->vdo_count; i++)
>   			msg.payload[i] = cpu_to_le32(port->vdo_data[i]);
> @@ -1244,6 +1277,8 @@ enum pdo_err {
>   	PDO_ERR_FIXED_NOT_SORTED,
>   	PDO_ERR_VARIABLE_BATT_NOT_SORTED,
>   	PDO_ERR_DUPE_PDO,
> +	PDO_ERR_PPS_APDO_NOT_SORTED,
> +	PDO_ERR_DUPE_PPS_APDO,
>   };
>   
>   static const char * const pdo_err_msg[] = {
> @@ -1259,6 +1294,10 @@ enum pdo_err {
>   	" err: Variable/Battery supply pdos should be in increasing order of their minimum voltage",
>   	[PDO_ERR_DUPE_PDO] =
>   	" err: Variable/Batt supply pdos cannot have same min/max voltage",
> +	[PDO_ERR_PPS_APDO_NOT_SORTED] =
> +	" err: Programmable power supply apdos should be in increasing order of their maximum voltage",
> +	[PDO_ERR_DUPE_PPS_APDO] =
> +	" err: Programmable power supply apdos cannot have same min/max voltage and max current",
>   };
>   
>   static enum pdo_err tcpm_caps_err(struct tcpm_port *port, const u32 *pdo,
> @@ -1308,6 +1347,26 @@ static enum pdo_err tcpm_caps_err(struct tcpm_port *port, const u32 *pdo,
>   					  pdo_min_voltage(pdo[i - 1])))
>   					return PDO_ERR_DUPE_PDO;
>   				break;
> +			/*
> +			 * The Programmable Power Supply APDOs, if present,
> +			 * shall be sent in Maximum Voltage order;
> +			 * lowest to highest.
> +			 */
> +			case PDO_TYPE_APDO:
> +				if (pdo_apdo_type(pdo[i]) != APDO_TYPE_PPS)
> +					break;
> +
> +				if (pdo_pps_apdo_max_current(pdo[i]) <
> +				    pdo_pps_apdo_max_current(pdo[i - 1]))
> +					return PDO_ERR_PPS_APDO_NOT_SORTED;
> +				else if (pdo_pps_apdo_min_voltage(pdo[i]) ==
> +					  pdo_pps_apdo_min_voltage(pdo[i - 1]) &&
> +					 pdo_pps_apdo_max_voltage(pdo[i]) ==
> +					  pdo_pps_apdo_max_voltage(pdo[i - 1]) &&
> +					 pdo_pps_apdo_max_current(pdo[i]) ==
> +					  pdo_pps_apdo_max_current(pdo[i - 1]))
> +					return PDO_ERR_DUPE_PPS_APDO;
> +				break;
>   			default:
>   				tcpm_log_force(port, " Unknown pdo type");
>   			}
> @@ -1333,11 +1392,16 @@ static int tcpm_validate_caps(struct tcpm_port *port, const u32 *pdo,
>   /*
>    * PD (data, control) command handling functions
>    */
> +
> +static int tcpm_pd_send_control(struct tcpm_port *port,
> +				enum pd_ctrl_msg_type type);
> +
>   static void tcpm_pd_data_request(struct tcpm_port *port,
>   				 const struct pd_message *msg)
>   {
>   	enum pd_data_msg_type type = pd_header_type_le(msg->header);
>   	unsigned int cnt = pd_header_cnt_le(msg->header);
> +	unsigned int rev = pd_header_rev_le(msg->header);
>   	unsigned int i;
>   
>   	switch (type) {
> @@ -1356,6 +1420,17 @@ static void tcpm_pd_data_request(struct tcpm_port *port,
>   				   port->nr_source_caps);
>   
>   		/*
> +		 * Adjust revision in subsequent message headers, as required,
> +		 * to comply with 6.2.1.1.5 of the USB PD 3.0 spec. We don't
> +		 * support Rev 1.0 so just do nothing in that scenario.
> +		 */
> +		if (rev == PD_REV10)
> +			break;
> +
> +		if (rev < PD_MAX_REV)
> +			port->negotiated_rev = rev;
> +
> +		/*
>   		 * This message may be received even if VBUS is not
>   		 * present. This is quite unexpected; see USB PD
>   		 * specification, sections 8.3.3.6.3.1 and 8.3.3.6.3.2.
> @@ -1376,6 +1451,20 @@ static void tcpm_pd_data_request(struct tcpm_port *port,
>   			tcpm_queue_message(port, PD_MSG_CTRL_REJECT);
>   			break;
>   		}
> +
> +		/*
> +		 * Adjust revision in subsequent message headers, as required,
> +		 * to comply with 6.2.1.1.5 of the USB PD 3.0 spec. We don't
> +		 * support Rev 1.0 so just reject in that scenario.
> +		 */
> +		if (rev == PD_REV10) {
> +			tcpm_queue_message(port, PD_MSG_CTRL_REJECT);
> +			break;
> +		}
> +
> +		if (rev < PD_MAX_REV)
> +			port->negotiated_rev = rev;
> +
>   		port->sink_request = le32_to_cpu(msg->payload[0]);
>   		tcpm_set_state(port, SRC_NEGOTIATE_CAPABILITIES, 0);
>   		break;
> @@ -1400,6 +1489,15 @@ static void tcpm_pd_data_request(struct tcpm_port *port,
>   	}
>   }
>   
> +static void tcpm_pps_complete(struct tcpm_port *port, int result)
> +{
> +	if (port->pps_pending) {
> +		port->pps_status = result;
> +		port->pps_pending = false;
> +		complete(&port->pps_complete);
> +	}
> +}
> +
>   static void tcpm_pd_ctrl_request(struct tcpm_port *port,
>   				 const struct pd_message *msg)
>   {
> @@ -1476,6 +1574,14 @@ static void tcpm_pd_ctrl_request(struct tcpm_port *port,
>   				next_state = SNK_WAIT_CAPABILITIES;
>   			tcpm_set_state(port, next_state, 0);
>   			break;
> +		case SNK_NEGOTIATE_PPS_CAPABILITIES:
> +			/* Revert data back from any requested PPS updates */
> +			port->pps_data.out_volt = port->supply_voltage;
> +			port->pps_data.op_curr = port->current_limit;
> +			port->pps_status = (type == PD_CTRL_WAIT ?
> +					    -EAGAIN : -EOPNOTSUPP);
> +			tcpm_set_state(port, SNK_READY, 0);
> +			break;
>   		case DR_SWAP_SEND:
>   			port->swap_status = (type == PD_CTRL_WAIT ?
>   					     -EAGAIN : -EOPNOTSUPP);
> @@ -1498,6 +1604,13 @@ static void tcpm_pd_ctrl_request(struct tcpm_port *port,
>   	case PD_CTRL_ACCEPT:
>   		switch (port->state) {
>   		case SNK_NEGOTIATE_CAPABILITIES:
> +			port->pps_data.active = false;
> +			tcpm_set_state(port, SNK_TRANSITION_SINK, 0);
> +			break;
> +		case SNK_NEGOTIATE_PPS_CAPABILITIES:
> +			port->pps_data.active = true;
> +			port->supply_voltage = port->pps_data.out_volt;
> +			port->current_limit = port->pps_data.op_curr;
>   			tcpm_set_state(port, SNK_TRANSITION_SINK, 0);
>   			break;
>   		case SOFT_RESET_SEND:
> @@ -1652,6 +1765,7 @@ static int tcpm_pd_send_control(struct tcpm_port *port,
>   	memset(&msg, 0, sizeof(msg));
>   	msg.header = PD_HEADER_LE(type, port->pwr_role,
>   				  port->data_role,
> +				  port->negotiated_rev,
>   				  port->message_id, 0);
>   
>   	return tcpm_pd_transmit(port, TCPC_TX_SOP, &msg);
> @@ -1761,6 +1875,8 @@ static int tcpm_pd_select_pdo(struct tcpm_port *port)
>   	unsigned int i, max_mw = 0, max_mv = 0;
>   	int ret = -EINVAL;
>   
> +	port->pps_data.supported = false;
> +
>   	/*
>   	 * Select the source PDO providing the most power while staying within
>   	 * the board's voltage limits. Prefer PDO providing exp
> @@ -1770,20 +1886,41 @@ static int tcpm_pd_select_pdo(struct tcpm_port *port)
>   		enum pd_pdo_type type = pdo_type(pdo);
>   		unsigned int mv, ma, mw;
>   
> -		if (type == PDO_TYPE_FIXED)
> +		switch (type) {
> +		case PDO_TYPE_FIXED:
>   			mv = pdo_fixed_voltage(pdo);
> -		else
> +			break;
> +		case PDO_TYPE_BATT:
> +		case PDO_TYPE_VAR:
>   			mv = pdo_min_voltage(pdo);
> +			break;
> +		case PDO_TYPE_APDO:
> +			if (pdo_apdo_type(pdo) == APDO_TYPE_PPS)
> +				port->pps_data.supported = true;
> +			continue;
> +		default:
> +			tcpm_log(port, "Invalid PDO type, ignoring");
> +			continue;
> +		}
>   
> -		if (type == PDO_TYPE_BATT) {
> -			mw = pdo_max_power(pdo);
> -		} else {
> +		switch (type) {
> +		case PDO_TYPE_FIXED:
> +		case PDO_TYPE_VAR:
>   			ma = min(pdo_max_current(pdo),
>   				 port->max_snk_ma);
>   			mw = ma * mv / 1000;
> +			break;
> +		case PDO_TYPE_BATT:
> +			mw = pdo_max_power(pdo);
> +			break;
> +		case PDO_TYPE_APDO:
> +			continue;
> +		default:
> +			tcpm_log(port, "Invalid PDO type, ignoring");
> +			continue;
>   		}
>   
> -		/* Perfer higher voltages if available */
> +		/* Prefer higher voltages if available */
>   		if ((mw > max_mw || (mw == max_mw && mv > max_mv)) &&
>   		    mv <= port->max_snk_mv) {
>   			ret = i;
> @@ -1795,6 +1932,65 @@ static int tcpm_pd_select_pdo(struct tcpm_port *port)
>   	return ret;
>   }
>   
> +static unsigned int tcpm_pd_select_pps_apdo(struct tcpm_port *port)
> +{
> +	unsigned int i, max_mw = 0, max_mv = 0;
> +	unsigned int pps_min_mv, pps_max_mv, ma, mw;
> +	enum pd_pdo_type type;
> +	u32 pdo;
> +	unsigned int index = 0;
> +
> +	/*
> +	 * Select the source PPS APDO providing the most power while staying
> +	 * within the board's limits. We skip the first PDO as this is always
> +	 * 5V 3A.
> +	 */
> +	for (i = 1; i < port->nr_source_caps; ++i) {
> +		pdo = port->source_caps[i];
> +		type = pdo_type(pdo);
> +
> +		switch (type) {
> +		case PDO_TYPE_APDO:
> +			if (pdo_apdo_type(pdo) != APDO_TYPE_PPS) {
> +				tcpm_log(port, "Not PPS APDO, ignoring");
> +				continue;
> +			}
> +
> +			pps_min_mv = pdo_pps_apdo_min_voltage(pdo);
> +			pps_max_mv = pdo_pps_apdo_max_voltage(pdo);
> +			ma = min(pdo_pps_apdo_max_current(pdo), port->max_snk_ma);
> +			mw = (ma * pps_max_mv) / 1000;
> +			break;
> +		default:
> +			tcpm_log(port, "Not APDO type, ignoring");
> +			continue;
> +		}
> +
> +		/* Prefer higher voltages if available */
> +		if ((mw > max_mw || (mw == max_mw && pps_max_mv > max_mv)) &&
> +		    pps_max_mv <= port->max_snk_mv) {
> +			index = i;
> +			max_mw = mw;
> +			max_mv = pps_max_mv;
> +		}
> +	}
> +
> +	if (index) {
> +		pdo = port->source_caps[index];
> +
> +		port->pps_data.min_volt = pdo_pps_apdo_min_voltage(pdo);
> +		port->pps_data.max_volt = pdo_pps_apdo_max_voltage(pdo);
> +		port->pps_data.max_curr =
> +			min(pdo_pps_apdo_max_current(pdo), port->max_snk_ma);
> +		port->pps_data.out_volt =
> +			min(pdo_pps_apdo_max_voltage(pdo), port->pps_data.out_volt);
> +		port->pps_data.op_curr =
> +			min(pdo_pps_apdo_max_current(pdo), port->pps_data.op_curr);
> +	}
> +
> +	return index;
> +}
> +
>   static int tcpm_pd_build_request(struct tcpm_port *port, u32 *rdo)
>   {
>   	unsigned int mv, ma, mw, flags;
> @@ -1809,10 +2005,18 @@ static int tcpm_pd_build_request(struct tcpm_port *port, u32 *rdo)
>   	pdo = port->source_caps[index];
>   	type = pdo_type(pdo);
>   
> -	if (type == PDO_TYPE_FIXED)
> +	switch (type) {
> +	case PDO_TYPE_FIXED:
>   		mv = pdo_fixed_voltage(pdo);
> -	else
> +		break;
> +	case PDO_TYPE_BATT:
> +	case PDO_TYPE_VAR:
>   		mv = pdo_min_voltage(pdo);
> +		break;
> +	default:
> +		tcpm_log(port, "Invalid PDO selected!");
> +		return -EINVAL;
> +	}
>   
>   	/* Select maximum available current within the board's power limit */
>   	if (type == PDO_TYPE_BATT) {
> @@ -1875,6 +2079,105 @@ static int tcpm_pd_send_request(struct tcpm_port *port)
>   	msg.header = PD_HEADER_LE(PD_DATA_REQUEST,
>   				  port->pwr_role,
>   				  port->data_role,
> +				  port->negotiated_rev,
> +				  port->message_id, 1);
> +	msg.payload[0] = cpu_to_le32(rdo);
> +
> +	return tcpm_pd_transmit(port, TCPC_TX_SOP, &msg);
> +}
> +
> +static int tcpm_pd_build_pps_request(struct tcpm_port *port, u32 *rdo)
> +{
> +	unsigned int out_mv, op_ma, op_mw, min_mv, max_mv, max_ma, flags;
> +	enum pd_pdo_type type;
> +	int index;
> +	u32 pdo;
> +
> +	index = tcpm_pd_select_pps_apdo(port);
> +	if (!index)
> +		return -EOPNOTSUPP;
> +
> +	pdo = port->source_caps[index];
> +	type = pdo_type(pdo);
> +
> +	switch (type) {
> +	case PDO_TYPE_APDO:
> +		if (pdo_apdo_type(pdo) != APDO_TYPE_PPS) {
> +			tcpm_log(port, "Invalid APDO selected!");
> +			return -EINVAL;
> +		}
> +		min_mv = port->pps_data.min_volt;
> +		max_mv = port->pps_data.max_volt;
> +		max_ma = port->pps_data.max_curr;
> +		out_mv = port->pps_data.out_volt;
> +		op_ma = port->pps_data.op_curr;
> +		break;
> +	default:
> +		tcpm_log(port, "Invalid PDO selected!");
> +		return -EINVAL;
> +	}
> +
> +	flags = RDO_USB_COMM | RDO_NO_SUSPEND;
> +
> +	op_mw = (op_ma * out_mv) / 1000;
> +	if (op_mw < port->operating_snk_mw) {
> +		/*
> +		 * Try raising current to meet power needs. If that's not enough
> +		 * then try upping the voltage. If that's still not enough
> +		 * then we've obviously chosen a PPS APDO which really isn't
> +		 * suitable so abandon ship.
> +		 */
> +		op_ma = (port->operating_snk_mw * 1000) / out_mv;
> +		if ((port->operating_snk_mw * 1000) % out_mv)
> +			++op_ma;
> +		op_ma += RDO_PROG_CURR_MA_STEP - (op_ma % RDO_PROG_CURR_MA_STEP);
> +
> +		if (op_ma > max_ma) {
> +			op_ma = max_ma;
> +			out_mv = (port->operating_snk_mw * 1000) / op_ma;
> +			if ((port->operating_snk_mw * 1000) % op_ma)
> +				++out_mv;
> +			out_mv += RDO_PROG_VOLT_MV_STEP -
> +				  (out_mv % RDO_PROG_VOLT_MV_STEP);
> +
> +			if (out_mv > max_mv) {
> +				tcpm_log(port, "Invalid PPS APDO selected!");
> +				return -EINVAL;
> +			}
> +		}
> +	}
> +
> +	tcpm_log(port, "cc=%d cc1=%d cc2=%d vbus=%d vconn=%s polarity=%d",
> +		 port->cc_req, port->cc1, port->cc2, port->vbus_source,
> +		 port->vconn_role == TYPEC_SOURCE ? "source" : "sink",
> +		 port->polarity);
> +
> +	*rdo = RDO_PROG(index + 1, out_mv, op_ma, flags);
> +
> +	tcpm_log(port, "Requesting APDO %d: %u mV, %u mA",
> +		 index, out_mv, op_ma);
> +
> +	port->pps_data.op_curr = op_ma;
> +	port->pps_data.out_volt = out_mv;
> +
> +	return 0;
> +}
> +
> +static int tcpm_pd_send_pps_request(struct tcpm_port *port)
> +{
> +	struct pd_message msg;
> +	int ret;
> +	u32 rdo;
> +
> +	ret = tcpm_pd_build_pps_request(port, &rdo);
> +	if (ret < 0)
> +		return ret;
> +
> +	memset(&msg, 0, sizeof(msg));
> +	msg.header = PD_HEADER_LE(PD_DATA_REQUEST,
> +				  port->pwr_role,
> +				  port->data_role,
> +				  port->negotiated_rev,
>   				  port->message_id, 1);
>   	msg.payload[0] = cpu_to_le32(rdo);
>   
> @@ -2060,6 +2363,7 @@ static void tcpm_reset_port(struct tcpm_port *port)
>   	tcpm_typec_disconnect(port);
>   	port->attached = false;
>   	port->pd_capable = false;
> +	port->pps_data.supported = false;
>   
>   	/*
>   	 * First Rx ID should be 0; set this to a sentinel of -1 so that
> @@ -2075,6 +2379,8 @@ static void tcpm_reset_port(struct tcpm_port *port)
>   	tcpm_set_attached_state(port, false);
>   	port->try_src_count = 0;
>   	port->try_snk_count = 0;
> +	port->supply_voltage = 0;
> +	port->current_limit = 0;
>   }
>   
>   static void tcpm_detach(struct tcpm_port *port)
> @@ -2321,6 +2627,7 @@ static void run_state_machine(struct tcpm_port *port)
>   		typec_set_pwr_opmode(port->typec_port, opmode);
>   		port->pwr_opmode = TYPEC_PWR_MODE_USB;
>   		port->caps_count = 0;
> +		port->negotiated_rev = PD_MAX_REV;
>   		port->message_id = 0;
>   		port->rx_msgid = -1;
>   		port->explicit_contract = false;
> @@ -2381,6 +2688,7 @@ static void run_state_machine(struct tcpm_port *port)
>   
>   		tcpm_swap_complete(port, 0);
>   		tcpm_typec_connect(port);
> +
>   		tcpm_check_send_discover(port);
>   		/*
>   		 * 6.3.5
> @@ -2404,6 +2712,7 @@ static void run_state_machine(struct tcpm_port *port)
>   	case SNK_UNATTACHED:
>   		if (!port->non_pd_role_swap)
>   			tcpm_swap_complete(port, -ENOTCONN);
> +		tcpm_pps_complete(port, -ENOTCONN);
>   		tcpm_snk_detach(port);
>   		if (tcpm_start_drp_toggling(port)) {
>   			tcpm_set_state(port, DRP_TOGGLING, 0);
> @@ -2493,6 +2802,7 @@ static void run_state_machine(struct tcpm_port *port)
>   					      port->cc2 : port->cc1);
>   		typec_set_pwr_opmode(port->typec_port, opmode);
>   		port->pwr_opmode = TYPEC_PWR_MODE_USB;
> +		port->negotiated_rev = PD_MAX_REV;
>   		port->message_id = 0;
>   		port->rx_msgid = -1;
>   		port->explicit_contract = false;
> @@ -2563,6 +2873,24 @@ static void run_state_machine(struct tcpm_port *port)
>   					    PD_T_SENDER_RESPONSE);
>   		}
>   		break;
> +	case SNK_NEGOTIATE_PPS_CAPABILITIES:
> +		ret = tcpm_pd_send_pps_request(port);
> +		if (ret < 0) {
> +			port->pps_status = ret;
> +			/*
> +			 * If this was called due to updates to sink
> +			 * capabilities, and pps is no longer valid, we should
> +			 * safely fall back to a standard PDO.
> +			 */
> +			if (port->update_sink_caps)
> +				tcpm_set_state(port, SNK_NEGOTIATE_CAPABILITIES, 0);
> +			else
> +				tcpm_set_state(port, SNK_READY, 0);
> +		} else {
> +			tcpm_set_state_cond(port, hard_reset_state(port),
> +					    PD_T_SENDER_RESPONSE);
> +		}
> +		break;
>   	case SNK_TRANSITION_SINK:
>   	case SNK_TRANSITION_SINK_VBUS:
>   		tcpm_set_state(port, hard_reset_state(port),
> @@ -2570,6 +2898,7 @@ static void run_state_machine(struct tcpm_port *port)
>   		break;
>   	case SNK_READY:
>   		port->try_snk_count = 0;
> +		port->update_sink_caps = false;
>   		if (port->explicit_contract) {
>   			typec_set_pwr_opmode(port->typec_port,
>   					     TYPEC_PWR_MODE_PD);
> @@ -2579,6 +2908,8 @@ static void run_state_machine(struct tcpm_port *port)
>   		tcpm_swap_complete(port, 0);
>   		tcpm_typec_connect(port);
>   		tcpm_check_send_discover(port);
> +		tcpm_pps_complete(port, port->pps_status);
> +
>   		break;
>   
>   	/* Accessory states */
> @@ -2625,6 +2956,7 @@ static void run_state_machine(struct tcpm_port *port)
>   		tcpm_set_state(port, SRC_UNATTACHED, PD_T_PS_SOURCE_ON);
>   		break;
>   	case SNK_HARD_RESET_SINK_OFF:
> +		memset(&port->pps_data, 0, sizeof(port->pps_data));
>   		tcpm_set_vconn(port, false);
>   		tcpm_set_charge(port, false);
>   		tcpm_set_roles(port, false, TYPEC_SINK, TYPEC_DEVICE);
> @@ -2845,6 +3177,7 @@ static void run_state_machine(struct tcpm_port *port)
>   		break;
>   	case ERROR_RECOVERY:
>   		tcpm_swap_complete(port, -EPROTO);
> +		tcpm_pps_complete(port, -EPROTO);
>   		tcpm_set_state(port, PORT_RESET, 0);
>   		break;
>   	case PORT_RESET:
> @@ -3427,6 +3760,162 @@ static int tcpm_try_role(const struct typec_capability *cap, int role)
>   	return ret;
>   }
>   
> +static int tcpm_pps_set_op_curr(struct tcpm_port *port, u16 op_curr)
> +{
> +	unsigned int target_mw;
> +	int ret;
> +
> +	mutex_lock(&port->swap_lock);
> +	mutex_lock(&port->lock);
> +
> +	if (!port->pps_data.active) {
> +		ret = -EOPNOTSUPP;
> +		goto port_unlock;
> +	}
> +
> +	if (port->state != SNK_READY) {
> +		ret = -EAGAIN;
> +		goto port_unlock;
> +	}
> +
> +	if (op_curr > port->pps_data.max_curr) {
> +		ret = -EINVAL;
> +		goto port_unlock;
> +	}
> +
> +	target_mw = (op_curr * port->pps_data.out_volt) / 1000;
> +	if (target_mw < port->operating_snk_mw) {
> +		ret = -EINVAL;
> +		goto port_unlock;
> +	}
> +
> +	reinit_completion(&port->pps_complete);
> +	port->pps_data.op_curr = op_curr;
> +	port->pps_status = 0;
> +	port->pps_pending = true;
> +	tcpm_set_state(port, SNK_NEGOTIATE_PPS_CAPABILITIES, 0);
> +	mutex_unlock(&port->lock);
> +
> +	if (!wait_for_completion_timeout(&port->pps_complete,
> +				msecs_to_jiffies(PD_PPS_CTRL_TIMEOUT)))
> +		ret = -ETIMEDOUT;
> +	else
> +		ret = port->pps_status;
> +
> +	goto swap_unlock;
> +
> +port_unlock:
> +	mutex_unlock(&port->lock);
> +swap_unlock:
> +	mutex_unlock(&port->swap_lock);
> +
> +	return ret;
> +}
> +
> +static int tcpm_pps_set_out_volt(struct tcpm_port *port, u16 out_volt)
> +{
> +	unsigned int target_mw;
> +	int ret;
> +
> +	mutex_lock(&port->swap_lock);
> +	mutex_lock(&port->lock);
> +
> +	if (!port->pps_data.active) {
> +		ret = -EOPNOTSUPP;
> +		goto port_unlock;
> +	}
> +
> +	if (port->state != SNK_READY) {
> +		ret = -EAGAIN;
> +		goto port_unlock;
> +	}
> +
> +	if (out_volt < port->pps_data.min_volt ||
> +	    out_volt > port->pps_data.max_volt) {
> +		ret = -EINVAL;
> +		goto port_unlock;
> +	}
> +
> +	target_mw = (port->pps_data.op_curr * out_volt) / 1000;
> +	if (target_mw < port->operating_snk_mw) {
> +		ret = -EINVAL;
> +		goto port_unlock;
> +	}
> +
> +	reinit_completion(&port->pps_complete);
> +	port->pps_data.out_volt = out_volt;
> +	port->pps_status = 0;
> +	port->pps_pending = true;
> +	tcpm_set_state(port, SNK_NEGOTIATE_PPS_CAPABILITIES, 0);
> +	mutex_unlock(&port->lock);
> +
> +	if (!wait_for_completion_timeout(&port->pps_complete,
> +				msecs_to_jiffies(PD_PPS_CTRL_TIMEOUT)))
> +		ret = -ETIMEDOUT;
> +	else
> +		ret = port->pps_status;
> +
> +	goto swap_unlock;
> +
> +port_unlock:
> +	mutex_unlock(&port->lock);
> +swap_unlock:
> +	mutex_unlock(&port->swap_lock);
> +
> +	return ret;
> +}
> +
> +static int tcpm_pps_activate(struct tcpm_port *port, bool activate)
> +{
> +	int ret = 0;
> +
> +	mutex_lock(&port->swap_lock);
> +	mutex_lock(&port->lock);
> +
> +	if (!port->pps_data.supported) {
> +		ret = -EOPNOTSUPP;
> +		goto port_unlock;
> +	}
> +
> +	/* Trying to deactivate PPS when already deactivated so just bail */
> +	if (!port->pps_data.active && !activate)
> +		goto port_unlock;
> +
> +	if (port->state != SNK_READY) {
> +		ret = -EAGAIN;
> +		goto port_unlock;
> +	}
> +
> +	reinit_completion(&port->pps_complete);
> +	port->pps_status = 0;
> +	port->pps_pending = true;
> +
> +	/* Trigger PPS request or move back to standard PDO contract */
> +	if (activate) {
> +		port->pps_data.out_volt = port->supply_voltage;
> +		port->pps_data.op_curr = port->current_limit;
> +		tcpm_set_state(port, SNK_NEGOTIATE_PPS_CAPABILITIES, 0);
> +	} else {
> +		tcpm_set_state(port, SNK_NEGOTIATE_CAPABILITIES, 0);
> +	}
> +	mutex_unlock(&port->lock);
> +
> +	if (!wait_for_completion_timeout(&port->pps_complete,
> +				msecs_to_jiffies(PD_PPS_CTRL_TIMEOUT)))
> +		ret = -ETIMEDOUT;
> +	else
> +		ret = port->pps_status;
> +
> +	goto swap_unlock;
> +
> +port_unlock:
> +	mutex_unlock(&port->lock);
> +swap_unlock:
> +	mutex_unlock(&port->swap_lock);
> +
> +	return ret;
> +}
> +
>   static void tcpm_init(struct tcpm_port *port)
>   {
>   	enum typec_cc_status cc1, cc2;
> @@ -3566,13 +4055,18 @@ int tcpm_update_sink_capabilities(struct tcpm_port *port, const u32 *pdo,
>   	port->max_snk_ma = max_snk_ma;
>   	port->max_snk_mw = max_snk_mw;
>   	port->operating_snk_mw = operating_snk_mw;
> +	port->update_sink_caps = true;
>   
>   	switch (port->state) {
>   	case SNK_NEGOTIATE_CAPABILITIES:
> +	case SNK_NEGOTIATE_PPS_CAPABILITIES:
>   	case SNK_READY:
>   	case SNK_TRANSITION_SINK:
>   	case SNK_TRANSITION_SINK_VBUS:
> -		tcpm_set_state(port, SNK_NEGOTIATE_CAPABILITIES, 0);
> +		if (port->pps_data.active)
> +			tcpm_set_state(port, SNK_NEGOTIATE_PPS_CAPABILITIES, 0);
> +		else
> +			tcpm_set_state(port, SNK_NEGOTIATE_CAPABILITIES, 0);
>   		break;
>   	default:
>   		break;
> @@ -3614,6 +4108,7 @@ struct tcpm_port *tcpm_register_port(struct device *dev, struct tcpc_dev *tcpc)
>   
>   	init_completion(&port->tx_complete);
>   	init_completion(&port->swap_complete);
> +	init_completion(&port->pps_complete);
>   	tcpm_debugfs_init(port);
>   
>   	if (tcpm_validate_caps(port, tcpc->config->src_pdo,
> @@ -3642,7 +4137,7 @@ struct tcpm_port *tcpm_register_port(struct device *dev, struct tcpc_dev *tcpc)
>   	port->typec_caps.prefer_role = tcpc->config->default_role;
>   	port->typec_caps.type = tcpc->config->type;
>   	port->typec_caps.revision = 0x0120;	/* Type-C spec release 1.2 */
> -	port->typec_caps.pd_revision = 0x0200;	/* USB-PD spec release 2.0 */
> +	port->typec_caps.pd_revision = 0x0300;	/* USB-PD spec release 3.0 */
>   	port->typec_caps.dr_set = tcpm_dr_set;
>   	port->typec_caps.pr_set = tcpm_pr_set;
>   	port->typec_caps.vconn_set = tcpm_vconn_set;
> diff --git a/include/linux/usb/pd.h b/include/linux/usb/pd.h
> index ff359bdf..09b570f 100644
> --- a/include/linux/usb/pd.h
> +++ b/include/linux/usb/pd.h
> @@ -103,8 +103,8 @@ enum pd_ext_msg_type {
>   	 (((cnt) & PD_HEADER_CNT_MASK) << PD_HEADER_CNT_SHIFT) |	\
>   	 ((ext_hdr) ? PD_HEADER_EXT_HDR : 0))
>   
> -#define PD_HEADER_LE(type, pwr, data, id, cnt) \
> -	cpu_to_le16(PD_HEADER((type), (pwr), (data), PD_REV20, (id), (cnt), (0)))
> +#define PD_HEADER_LE(type, pwr, data, rev, id, cnt) \
> +	cpu_to_le16(PD_HEADER((type), (pwr), (data), (rev), (id), (cnt), (0)))
>   
>   static inline unsigned int pd_header_cnt(u16 header)
>   {
> diff --git a/include/linux/usb/tcpm.h b/include/linux/usb/tcpm.h
> index ca1c0b5..220ecc0 100644
> --- a/include/linux/usb/tcpm.h
> +++ b/include/linux/usb/tcpm.h
> @@ -36,6 +36,7 @@ enum typec_cc_polarity {
>   /* Time to wait for TCPC to complete transmit */
>   #define PD_T_TCPC_TX_TIMEOUT	100		/* in ms	*/
>   #define PD_ROLE_SWAP_TIMEOUT	(MSEC_PER_SEC * 10)
> +#define PD_PPS_CTRL_TIMEOUT	(MSEC_PER_SEC * 10)
>   
>   enum tcpm_transmit_status {
>   	TCPC_TX_SUCCESS = 0,
>
Greg KH April 22, 2018, 2:05 p.m. UTC | #2
On Fri, Mar 23, 2018 at 10:12:20AM +0000, Adam Thomson wrote:
> This commit adds code to handle requesting of PPS APDOs. Switching
> between standard PDOs and APDOs, and re-requesting an APDO to
> modify operating voltage/current will be triggered by an
> external call into TCPM.
> 
> Signed-off-by: Adam Thomson <Adam.Thomson.Opensource@diasemi.com>
> Acked-by: Heikki Krogerus <heikki.krogerus@linux.intel.com>
> Reviewed-by: Guenter Roeck <linux@roeck-us.net>
> ---
>  drivers/usb/typec/tcpm.c | 517 ++++++++++++++++++++++++++++++++++++++++++++++-
>  include/linux/usb/pd.h   |   4 +-
>  include/linux/usb/tcpm.h |   1 +
>  3 files changed, 509 insertions(+), 13 deletions(-)

This patch adds build warnings to the tree, so I can't take it, sorry.
Please fix up and resend.

thanks,

greg k-h
Adam Thomson April 22, 2018, 8:57 p.m. UTC | #3
On 22 April 2018 15:05, Greg Kroah-Hartman wrote:

> On Fri, Mar 23, 2018 at 10:12:20AM +0000, Adam Thomson wrote:
> > This commit adds code to handle requesting of PPS APDOs. Switching
> > between standard PDOs and APDOs, and re-requesting an APDO to
> > modify operating voltage/current will be triggered by an
> > external call into TCPM.
> >
> > Signed-off-by: Adam Thomson <Adam.Thomson.Opensource@diasemi.com>
> > Acked-by: Heikki Krogerus <heikki.krogerus@linux.intel.com>
> > Reviewed-by: Guenter Roeck <linux@roeck-us.net>
> > ---
> >  drivers/usb/typec/tcpm.c | 517
> ++++++++++++++++++++++++++++++++++++++++++++++-
> >  include/linux/usb/pd.h   |   4 +-
> >  include/linux/usb/tcpm.h |   1 +
> >  3 files changed, 509 insertions(+), 13 deletions(-)
> 
> This patch adds build warnings to the tree, so I can't take it, sorry.
> Please fix up and resend.

No problem. Sorry for that. Will take a look and resolve the warnings.
Adam Thomson April 23, 2018, 7:49 a.m. UTC | #4
On 22 April 2018 21:58, Adam Thomson wrote:

> On 22 April 2018 15:05, Greg Kroah-Hartman wrote:
> 
> > On Fri, Mar 23, 2018 at 10:12:20AM +0000, Adam Thomson wrote:
> > > This commit adds code to handle requesting of PPS APDOs. Switching
> > > between standard PDOs and APDOs, and re-requesting an APDO to
> > > modify operating voltage/current will be triggered by an
> > > external call into TCPM.
> > >
> > > Signed-off-by: Adam Thomson <Adam.Thomson.Opensource@diasemi.com>
> > > Acked-by: Heikki Krogerus <heikki.krogerus@linux.intel.com>
> > > Reviewed-by: Guenter Roeck <linux@roeck-us.net>
> > > ---
> > >  drivers/usb/typec/tcpm.c | 517
> > ++++++++++++++++++++++++++++++++++++++++++++++-
> > >  include/linux/usb/pd.h   |   4 +-
> > >  include/linux/usb/tcpm.h |   1 +
> > >  3 files changed, 509 insertions(+), 13 deletions(-)
> >
> > This patch adds build warnings to the tree, so I can't take it, sorry.
> > Please fix up and resend.
> 
> No problem. Sorry for that. Will take a look and resolve the warnings.

Sadly this is going to be a bit more than 'resolve the warnings' task now as Jun
Li's patch set has now made it in before me which means I need to rebase PDO
Selection because of his changes. :(
Greg KH April 23, 2018, 8:25 a.m. UTC | #5
On Mon, Apr 23, 2018 at 07:49:38AM +0000, Adam Thomson wrote:
> On 22 April 2018 21:58, Adam Thomson wrote:
> 
> > On 22 April 2018 15:05, Greg Kroah-Hartman wrote:
> > 
> > > On Fri, Mar 23, 2018 at 10:12:20AM +0000, Adam Thomson wrote:
> > > > This commit adds code to handle requesting of PPS APDOs. Switching
> > > > between standard PDOs and APDOs, and re-requesting an APDO to
> > > > modify operating voltage/current will be triggered by an
> > > > external call into TCPM.
> > > >
> > > > Signed-off-by: Adam Thomson <Adam.Thomson.Opensource@diasemi.com>
> > > > Acked-by: Heikki Krogerus <heikki.krogerus@linux.intel.com>
> > > > Reviewed-by: Guenter Roeck <linux@roeck-us.net>
> > > > ---
> > > >  drivers/usb/typec/tcpm.c | 517
> > > ++++++++++++++++++++++++++++++++++++++++++++++-
> > > >  include/linux/usb/pd.h   |   4 +-
> > > >  include/linux/usb/tcpm.h |   1 +
> > > >  3 files changed, 509 insertions(+), 13 deletions(-)
> > >
> > > This patch adds build warnings to the tree, so I can't take it, sorry.
> > > Please fix up and resend.
> > 
> > No problem. Sorry for that. Will take a look and resolve the warnings.
> 
> Sadly this is going to be a bit more than 'resolve the warnings' task now as Jun
> Li's patch set has now made it in before me which means I need to rebase PDO
> Selection because of his changes. :(

Someone was going to have to do that, sorry :(
Adam Thomson April 23, 2018, 11:06 a.m. UTC | #6
On 23 April 2018 09:25, Greg Kroah-Hartman wrote:

> On Mon, Apr 23, 2018 at 07:49:38AM +0000, Adam Thomson wrote:
> > On 22 April 2018 21:58, Adam Thomson wrote:
> >
> > > On 22 April 2018 15:05, Greg Kroah-Hartman wrote:
> > >
> > > > On Fri, Mar 23, 2018 at 10:12:20AM +0000, Adam Thomson wrote:
> > > > > This commit adds code to handle requesting of PPS APDOs. Switching
> > > > > between standard PDOs and APDOs, and re-requesting an APDO to
> > > > > modify operating voltage/current will be triggered by an
> > > > > external call into TCPM.
> > > > >
> > > > > Signed-off-by: Adam Thomson <Adam.Thomson.Opensource@diasemi.com>
> > > > > Acked-by: Heikki Krogerus <heikki.krogerus@linux.intel.com>
> > > > > Reviewed-by: Guenter Roeck <linux@roeck-us.net>
> > > > > ---
> > > > >  drivers/usb/typec/tcpm.c | 517
> > > > ++++++++++++++++++++++++++++++++++++++++++++++-
> > > > >  include/linux/usb/pd.h   |   4 +-
> > > > >  include/linux/usb/tcpm.h |   1 +
> > > > >  3 files changed, 509 insertions(+), 13 deletions(-)
> > > >
> > > > This patch adds build warnings to the tree, so I can't take it, sorry.
> > > > Please fix up and resend.
> > >
> > > No problem. Sorry for that. Will take a look and resolve the warnings.
> >
> > Sadly this is going to be a bit more than 'resolve the warnings' task now as Jun
> > Li's patch set has now made it in before me which means I need to rebase PDO
> > Selection because of his changes. :(
> 
> Someone was going to have to do that, sorry :(

Just as an FYI, this patch will produce warnings until the associated
power_supply interface patch (number 5 in the series previously sent) is
included as that makes use of the new API. Not sure how I can get around that
so I guess we have to wait on Sebastian to give the nod for the rest of the
power_supply patches before this can make it through.

Anyway, will rebase and resend, and will further highlight this reliance in the
cover letter.
Greg KH April 23, 2018, 11:28 a.m. UTC | #7
On Mon, Apr 23, 2018 at 11:06:25AM +0000, Adam Thomson wrote:
> On 23 April 2018 09:25, Greg Kroah-Hartman wrote:
> 
> > On Mon, Apr 23, 2018 at 07:49:38AM +0000, Adam Thomson wrote:
> > > On 22 April 2018 21:58, Adam Thomson wrote:
> > >
> > > > On 22 April 2018 15:05, Greg Kroah-Hartman wrote:
> > > >
> > > > > On Fri, Mar 23, 2018 at 10:12:20AM +0000, Adam Thomson wrote:
> > > > > > This commit adds code to handle requesting of PPS APDOs. Switching
> > > > > > between standard PDOs and APDOs, and re-requesting an APDO to
> > > > > > modify operating voltage/current will be triggered by an
> > > > > > external call into TCPM.
> > > > > >
> > > > > > Signed-off-by: Adam Thomson <Adam.Thomson.Opensource@diasemi.com>
> > > > > > Acked-by: Heikki Krogerus <heikki.krogerus@linux.intel.com>
> > > > > > Reviewed-by: Guenter Roeck <linux@roeck-us.net>
> > > > > > ---
> > > > > >  drivers/usb/typec/tcpm.c | 517
> > > > > ++++++++++++++++++++++++++++++++++++++++++++++-
> > > > > >  include/linux/usb/pd.h   |   4 +-
> > > > > >  include/linux/usb/tcpm.h |   1 +
> > > > > >  3 files changed, 509 insertions(+), 13 deletions(-)
> > > > >
> > > > > This patch adds build warnings to the tree, so I can't take it, sorry.
> > > > > Please fix up and resend.
> > > >
> > > > No problem. Sorry for that. Will take a look and resolve the warnings.
> > >
> > > Sadly this is going to be a bit more than 'resolve the warnings' task now as Jun
> > > Li's patch set has now made it in before me which means I need to rebase PDO
> > > Selection because of his changes. :(
> > 
> > Someone was going to have to do that, sorry :(
> 
> Just as an FYI, this patch will produce warnings until the associated
> power_supply interface patch (number 5 in the series previously sent) is
> included as that makes use of the new API. Not sure how I can get around that
> so I guess we have to wait on Sebastian to give the nod for the rest of the
> power_supply patches before this can make it through.

That's not good.  There has to be a way to prevent a build warning from
happening...

thanks,

greg k-h
Adam Thomson April 23, 2018, 12:47 p.m. UTC | #8
On 23 April 2018 12:28, Greg Kroah-Hartman wrote:

> On Mon, Apr 23, 2018 at 11:06:25AM +0000, Adam Thomson wrote:
> > On 23 April 2018 09:25, Greg Kroah-Hartman wrote:
> >
> > > On Mon, Apr 23, 2018 at 07:49:38AM +0000, Adam Thomson wrote:
> > > > On 22 April 2018 21:58, Adam Thomson wrote:
> > > >
> > > > > On 22 April 2018 15:05, Greg Kroah-Hartman wrote:
> > > > >
> > > > > > On Fri, Mar 23, 2018 at 10:12:20AM +0000, Adam Thomson wrote:
> > > > > > > This commit adds code to handle requesting of PPS APDOs. Switching
> > > > > > > between standard PDOs and APDOs, and re-requesting an APDO to
> > > > > > > modify operating voltage/current will be triggered by an
> > > > > > > external call into TCPM.
> > > > > > >
> > > > > > > Signed-off-by: Adam Thomson
> <Adam.Thomson.Opensource@diasemi.com>
> > > > > > > Acked-by: Heikki Krogerus <heikki.krogerus@linux.intel.com>
> > > > > > > Reviewed-by: Guenter Roeck <linux@roeck-us.net>
> > > > > > > ---
> > > > > > >  drivers/usb/typec/tcpm.c | 517
> > > > > > ++++++++++++++++++++++++++++++++++++++++++++++-
> > > > > > >  include/linux/usb/pd.h   |   4 +-
> > > > > > >  include/linux/usb/tcpm.h |   1 +
> > > > > > >  3 files changed, 509 insertions(+), 13 deletions(-)
> > > > > >
> > > > > > This patch adds build warnings to the tree, so I can't take it, sorry.
> > > > > > Please fix up and resend.
> > > > >
> > > > > No problem. Sorry for that. Will take a look and resolve the warnings.
> > > >
> > > > Sadly this is going to be a bit more than 'resolve the warnings' task now as Jun
> > > > Li's patch set has now made it in before me which means I need to rebase PDO
> > > > Selection because of his changes. :(
> > >
> > > Someone was going to have to do that, sorry :(
> >
> > Just as an FYI, this patch will produce warnings until the associated
> > power_supply interface patch (number 5 in the series previously sent) is
> > included as that makes use of the new API. Not sure how I can get around that
> > so I guess we have to wait on Sebastian to give the nod for the rest of the
> > power_supply patches before this can make it through.
> 
> That's not good.  There has to be a way to prevent a build warning from
> happening...

If we're ok with '__maybe_unused' then I can add that. Wasn't sure if something
like that would be acceptable for this scenario.
Greg KH April 23, 2018, 1:20 p.m. UTC | #9
On Mon, Apr 23, 2018 at 12:47:47PM +0000, Adam Thomson wrote:
> On 23 April 2018 12:28, Greg Kroah-Hartman wrote:
> 
> > On Mon, Apr 23, 2018 at 11:06:25AM +0000, Adam Thomson wrote:
> > > On 23 April 2018 09:25, Greg Kroah-Hartman wrote:
> > >
> > > > On Mon, Apr 23, 2018 at 07:49:38AM +0000, Adam Thomson wrote:
> > > > > On 22 April 2018 21:58, Adam Thomson wrote:
> > > > >
> > > > > > On 22 April 2018 15:05, Greg Kroah-Hartman wrote:
> > > > > >
> > > > > > > On Fri, Mar 23, 2018 at 10:12:20AM +0000, Adam Thomson wrote:
> > > > > > > > This commit adds code to handle requesting of PPS APDOs. Switching
> > > > > > > > between standard PDOs and APDOs, and re-requesting an APDO to
> > > > > > > > modify operating voltage/current will be triggered by an
> > > > > > > > external call into TCPM.
> > > > > > > >
> > > > > > > > Signed-off-by: Adam Thomson
> > <Adam.Thomson.Opensource@diasemi.com>
> > > > > > > > Acked-by: Heikki Krogerus <heikki.krogerus@linux.intel.com>
> > > > > > > > Reviewed-by: Guenter Roeck <linux@roeck-us.net>
> > > > > > > > ---
> > > > > > > >  drivers/usb/typec/tcpm.c | 517
> > > > > > > ++++++++++++++++++++++++++++++++++++++++++++++-
> > > > > > > >  include/linux/usb/pd.h   |   4 +-
> > > > > > > >  include/linux/usb/tcpm.h |   1 +
> > > > > > > >  3 files changed, 509 insertions(+), 13 deletions(-)
> > > > > > >
> > > > > > > This patch adds build warnings to the tree, so I can't take it, sorry.
> > > > > > > Please fix up and resend.
> > > > > >
> > > > > > No problem. Sorry for that. Will take a look and resolve the warnings.
> > > > >
> > > > > Sadly this is going to be a bit more than 'resolve the warnings' task now as Jun
> > > > > Li's patch set has now made it in before me which means I need to rebase PDO
> > > > > Selection because of his changes. :(
> > > >
> > > > Someone was going to have to do that, sorry :(
> > >
> > > Just as an FYI, this patch will produce warnings until the associated
> > > power_supply interface patch (number 5 in the series previously sent) is
> > > included as that makes use of the new API. Not sure how I can get around that
> > > so I guess we have to wait on Sebastian to give the nod for the rest of the
> > > power_supply patches before this can make it through.
> > 
> > That's not good.  There has to be a way to prevent a build warning from
> > happening...
> 
> If we're ok with '__maybe_unused' then I can add that. Wasn't sure if something
> like that would be acceptable for this scenario.

I don't remember what the warning was, or what the code was either,
sorry...
diff mbox

Patch

diff --git a/drivers/usb/typec/tcpm.c b/drivers/usb/typec/tcpm.c
index 4c0fc54..1a66c9e 100644
--- a/drivers/usb/typec/tcpm.c
+++ b/drivers/usb/typec/tcpm.c
@@ -47,6 +47,7 @@ 
 	S(SNK_DISCOVERY_DEBOUNCE_DONE),		\
 	S(SNK_WAIT_CAPABILITIES),		\
 	S(SNK_NEGOTIATE_CAPABILITIES),		\
+	S(SNK_NEGOTIATE_PPS_CAPABILITIES),	\
 	S(SNK_TRANSITION_SINK),			\
 	S(SNK_TRANSITION_SINK_VBUS),		\
 	S(SNK_READY),				\
@@ -166,6 +167,16 @@  struct pd_mode_data {
 	struct typec_altmode_desc altmode_desc[SVID_DISCOVERY_MAX];
 };
 
+struct pd_pps_data {
+	u32 min_volt;
+	u32 max_volt;
+	u32 max_curr;
+	u32 out_volt;
+	u32 op_curr;
+	bool supported;
+	bool active;
+};
+
 struct tcpm_port {
 	struct device *dev;
 
@@ -233,6 +244,7 @@  struct tcpm_port {
 	struct completion swap_complete;
 	int swap_status;
 
+	unsigned int negotiated_rev;
 	unsigned int message_id;
 	unsigned int caps_count;
 	unsigned int hard_reset_count;
@@ -259,6 +271,7 @@  struct tcpm_port {
 	unsigned int max_snk_ma;
 	unsigned int max_snk_mw;
 	unsigned int operating_snk_mw;
+	bool update_sink_caps;
 
 	/* Requested current / voltage */
 	u32 current_limit;
@@ -275,8 +288,13 @@  struct tcpm_port {
 	/* VDO to retry if UFP responder replied busy */
 	u32 vdo_retry;
 
-	/* Alternate mode data */
+	/* PPS */
+	struct pd_pps_data pps_data;
+	struct completion pps_complete;
+	bool pps_pending;
+	int pps_status;
 
+	/* Alternate mode data */
 	struct pd_mode_data mode_data;
 	struct typec_altmode *partner_altmode[SVID_DISCOVERY_MAX];
 	struct typec_altmode *port_altmode[SVID_DISCOVERY_MAX];
@@ -494,6 +512,16 @@  static void tcpm_log_source_caps(struct tcpm_port *port)
 				  pdo_max_voltage(pdo),
 				  pdo_max_power(pdo));
 			break;
+		case PDO_TYPE_APDO:
+			if (pdo_apdo_type(pdo) == APDO_TYPE_PPS)
+				scnprintf(msg, sizeof(msg),
+					  "%u-%u mV, %u mA",
+					  pdo_pps_apdo_min_voltage(pdo),
+					  pdo_pps_apdo_max_voltage(pdo),
+					  pdo_pps_apdo_max_current(pdo));
+			else
+				strcpy(msg, "undefined APDO");
+			break;
 		default:
 			strcpy(msg, "undefined");
 			break;
@@ -777,11 +805,13 @@  static int tcpm_pd_send_source_caps(struct tcpm_port *port)
 		msg.header = PD_HEADER_LE(PD_CTRL_REJECT,
 					  port->pwr_role,
 					  port->data_role,
+					  port->negotiated_rev,
 					  port->message_id, 0);
 	} else {
 		msg.header = PD_HEADER_LE(PD_DATA_SOURCE_CAP,
 					  port->pwr_role,
 					  port->data_role,
+					  port->negotiated_rev,
 					  port->message_id,
 					  port->nr_src_pdo);
 	}
@@ -802,11 +832,13 @@  static int tcpm_pd_send_sink_caps(struct tcpm_port *port)
 		msg.header = PD_HEADER_LE(PD_CTRL_REJECT,
 					  port->pwr_role,
 					  port->data_role,
+					  port->negotiated_rev,
 					  port->message_id, 0);
 	} else {
 		msg.header = PD_HEADER_LE(PD_DATA_SINK_CAP,
 					  port->pwr_role,
 					  port->data_role,
+					  port->negotiated_rev,
 					  port->message_id,
 					  port->nr_snk_pdo);
 	}
@@ -1173,6 +1205,7 @@  static void vdm_run_state_machine(struct tcpm_port *port)
 		msg.header = PD_HEADER_LE(PD_DATA_VENDOR_DEF,
 					  port->pwr_role,
 					  port->data_role,
+					  port->negotiated_rev,
 					  port->message_id, port->vdo_count);
 		for (i = 0; i < port->vdo_count; i++)
 			msg.payload[i] = cpu_to_le32(port->vdo_data[i]);
@@ -1244,6 +1277,8 @@  enum pdo_err {
 	PDO_ERR_FIXED_NOT_SORTED,
 	PDO_ERR_VARIABLE_BATT_NOT_SORTED,
 	PDO_ERR_DUPE_PDO,
+	PDO_ERR_PPS_APDO_NOT_SORTED,
+	PDO_ERR_DUPE_PPS_APDO,
 };
 
 static const char * const pdo_err_msg[] = {
@@ -1259,6 +1294,10 @@  enum pdo_err {
 	" err: Variable/Battery supply pdos should be in increasing order of their minimum voltage",
 	[PDO_ERR_DUPE_PDO] =
 	" err: Variable/Batt supply pdos cannot have same min/max voltage",
+	[PDO_ERR_PPS_APDO_NOT_SORTED] =
+	" err: Programmable power supply apdos should be in increasing order of their maximum voltage",
+	[PDO_ERR_DUPE_PPS_APDO] =
+	" err: Programmable power supply apdos cannot have same min/max voltage and max current",
 };
 
 static enum pdo_err tcpm_caps_err(struct tcpm_port *port, const u32 *pdo,
@@ -1308,6 +1347,26 @@  static enum pdo_err tcpm_caps_err(struct tcpm_port *port, const u32 *pdo,
 					  pdo_min_voltage(pdo[i - 1])))
 					return PDO_ERR_DUPE_PDO;
 				break;
+			/*
+			 * The Programmable Power Supply APDOs, if present,
+			 * shall be sent in Maximum Voltage order;
+			 * lowest to highest.
+			 */
+			case PDO_TYPE_APDO:
+				if (pdo_apdo_type(pdo[i]) != APDO_TYPE_PPS)
+					break;
+
+				if (pdo_pps_apdo_max_current(pdo[i]) <
+				    pdo_pps_apdo_max_current(pdo[i - 1]))
+					return PDO_ERR_PPS_APDO_NOT_SORTED;
+				else if (pdo_pps_apdo_min_voltage(pdo[i]) ==
+					  pdo_pps_apdo_min_voltage(pdo[i - 1]) &&
+					 pdo_pps_apdo_max_voltage(pdo[i]) ==
+					  pdo_pps_apdo_max_voltage(pdo[i - 1]) &&
+					 pdo_pps_apdo_max_current(pdo[i]) ==
+					  pdo_pps_apdo_max_current(pdo[i - 1]))
+					return PDO_ERR_DUPE_PPS_APDO;
+				break;
 			default:
 				tcpm_log_force(port, " Unknown pdo type");
 			}
@@ -1333,11 +1392,16 @@  static int tcpm_validate_caps(struct tcpm_port *port, const u32 *pdo,
 /*
  * PD (data, control) command handling functions
  */
+
+static int tcpm_pd_send_control(struct tcpm_port *port,
+				enum pd_ctrl_msg_type type);
+
 static void tcpm_pd_data_request(struct tcpm_port *port,
 				 const struct pd_message *msg)
 {
 	enum pd_data_msg_type type = pd_header_type_le(msg->header);
 	unsigned int cnt = pd_header_cnt_le(msg->header);
+	unsigned int rev = pd_header_rev_le(msg->header);
 	unsigned int i;
 
 	switch (type) {
@@ -1356,6 +1420,17 @@  static void tcpm_pd_data_request(struct tcpm_port *port,
 				   port->nr_source_caps);
 
 		/*
+		 * Adjust revision in subsequent message headers, as required,
+		 * to comply with 6.2.1.1.5 of the USB PD 3.0 spec. We don't
+		 * support Rev 1.0 so just do nothing in that scenario.
+		 */
+		if (rev == PD_REV10)
+			break;
+
+		if (rev < PD_MAX_REV)
+			port->negotiated_rev = rev;
+
+		/*
 		 * This message may be received even if VBUS is not
 		 * present. This is quite unexpected; see USB PD
 		 * specification, sections 8.3.3.6.3.1 and 8.3.3.6.3.2.
@@ -1376,6 +1451,20 @@  static void tcpm_pd_data_request(struct tcpm_port *port,
 			tcpm_queue_message(port, PD_MSG_CTRL_REJECT);
 			break;
 		}
+
+		/*
+		 * Adjust revision in subsequent message headers, as required,
+		 * to comply with 6.2.1.1.5 of the USB PD 3.0 spec. We don't
+		 * support Rev 1.0 so just reject in that scenario.
+		 */
+		if (rev == PD_REV10) {
+			tcpm_queue_message(port, PD_MSG_CTRL_REJECT);
+			break;
+		}
+
+		if (rev < PD_MAX_REV)
+			port->negotiated_rev = rev;
+
 		port->sink_request = le32_to_cpu(msg->payload[0]);
 		tcpm_set_state(port, SRC_NEGOTIATE_CAPABILITIES, 0);
 		break;
@@ -1400,6 +1489,15 @@  static void tcpm_pd_data_request(struct tcpm_port *port,
 	}
 }
 
+static void tcpm_pps_complete(struct tcpm_port *port, int result)
+{
+	if (port->pps_pending) {
+		port->pps_status = result;
+		port->pps_pending = false;
+		complete(&port->pps_complete);
+	}
+}
+
 static void tcpm_pd_ctrl_request(struct tcpm_port *port,
 				 const struct pd_message *msg)
 {
@@ -1476,6 +1574,14 @@  static void tcpm_pd_ctrl_request(struct tcpm_port *port,
 				next_state = SNK_WAIT_CAPABILITIES;
 			tcpm_set_state(port, next_state, 0);
 			break;
+		case SNK_NEGOTIATE_PPS_CAPABILITIES:
+			/* Revert data back from any requested PPS updates */
+			port->pps_data.out_volt = port->supply_voltage;
+			port->pps_data.op_curr = port->current_limit;
+			port->pps_status = (type == PD_CTRL_WAIT ?
+					    -EAGAIN : -EOPNOTSUPP);
+			tcpm_set_state(port, SNK_READY, 0);
+			break;
 		case DR_SWAP_SEND:
 			port->swap_status = (type == PD_CTRL_WAIT ?
 					     -EAGAIN : -EOPNOTSUPP);
@@ -1498,6 +1604,13 @@  static void tcpm_pd_ctrl_request(struct tcpm_port *port,
 	case PD_CTRL_ACCEPT:
 		switch (port->state) {
 		case SNK_NEGOTIATE_CAPABILITIES:
+			port->pps_data.active = false;
+			tcpm_set_state(port, SNK_TRANSITION_SINK, 0);
+			break;
+		case SNK_NEGOTIATE_PPS_CAPABILITIES:
+			port->pps_data.active = true;
+			port->supply_voltage = port->pps_data.out_volt;
+			port->current_limit = port->pps_data.op_curr;
 			tcpm_set_state(port, SNK_TRANSITION_SINK, 0);
 			break;
 		case SOFT_RESET_SEND:
@@ -1652,6 +1765,7 @@  static int tcpm_pd_send_control(struct tcpm_port *port,
 	memset(&msg, 0, sizeof(msg));
 	msg.header = PD_HEADER_LE(type, port->pwr_role,
 				  port->data_role,
+				  port->negotiated_rev,
 				  port->message_id, 0);
 
 	return tcpm_pd_transmit(port, TCPC_TX_SOP, &msg);
@@ -1761,6 +1875,8 @@  static int tcpm_pd_select_pdo(struct tcpm_port *port)
 	unsigned int i, max_mw = 0, max_mv = 0;
 	int ret = -EINVAL;
 
+	port->pps_data.supported = false;
+
 	/*
 	 * Select the source PDO providing the most power while staying within
 	 * the board's voltage limits. Prefer PDO providing exp
@@ -1770,20 +1886,41 @@  static int tcpm_pd_select_pdo(struct tcpm_port *port)
 		enum pd_pdo_type type = pdo_type(pdo);
 		unsigned int mv, ma, mw;
 
-		if (type == PDO_TYPE_FIXED)
+		switch (type) {
+		case PDO_TYPE_FIXED:
 			mv = pdo_fixed_voltage(pdo);
-		else
+			break;
+		case PDO_TYPE_BATT:
+		case PDO_TYPE_VAR:
 			mv = pdo_min_voltage(pdo);
+			break;
+		case PDO_TYPE_APDO:
+			if (pdo_apdo_type(pdo) == APDO_TYPE_PPS)
+				port->pps_data.supported = true;
+			continue;
+		default:
+			tcpm_log(port, "Invalid PDO type, ignoring");
+			continue;
+		}
 
-		if (type == PDO_TYPE_BATT) {
-			mw = pdo_max_power(pdo);
-		} else {
+		switch (type) {
+		case PDO_TYPE_FIXED:
+		case PDO_TYPE_VAR:
 			ma = min(pdo_max_current(pdo),
 				 port->max_snk_ma);
 			mw = ma * mv / 1000;
+			break;
+		case PDO_TYPE_BATT:
+			mw = pdo_max_power(pdo);
+			break;
+		case PDO_TYPE_APDO:
+			continue;
+		default:
+			tcpm_log(port, "Invalid PDO type, ignoring");
+			continue;
 		}
 
-		/* Perfer higher voltages if available */
+		/* Prefer higher voltages if available */
 		if ((mw > max_mw || (mw == max_mw && mv > max_mv)) &&
 		    mv <= port->max_snk_mv) {
 			ret = i;
@@ -1795,6 +1932,65 @@  static int tcpm_pd_select_pdo(struct tcpm_port *port)
 	return ret;
 }
 
+static unsigned int tcpm_pd_select_pps_apdo(struct tcpm_port *port)
+{
+	unsigned int i, max_mw = 0, max_mv = 0;
+	unsigned int pps_min_mv, pps_max_mv, ma, mw;
+	enum pd_pdo_type type;
+	u32 pdo;
+	unsigned int index = 0;
+
+	/*
+	 * Select the source PPS APDO providing the most power while staying
+	 * within the board's limits. We skip the first PDO as this is always
+	 * 5V 3A.
+	 */
+	for (i = 1; i < port->nr_source_caps; ++i) {
+		pdo = port->source_caps[i];
+		type = pdo_type(pdo);
+
+		switch (type) {
+		case PDO_TYPE_APDO:
+			if (pdo_apdo_type(pdo) != APDO_TYPE_PPS) {
+				tcpm_log(port, "Not PPS APDO, ignoring");
+				continue;
+			}
+
+			pps_min_mv = pdo_pps_apdo_min_voltage(pdo);
+			pps_max_mv = pdo_pps_apdo_max_voltage(pdo);
+			ma = min(pdo_pps_apdo_max_current(pdo), port->max_snk_ma);
+			mw = (ma * pps_max_mv) / 1000;
+			break;
+		default:
+			tcpm_log(port, "Not APDO type, ignoring");
+			continue;
+		}
+
+		/* Prefer higher voltages if available */
+		if ((mw > max_mw || (mw == max_mw && pps_max_mv > max_mv)) &&
+		    pps_max_mv <= port->max_snk_mv) {
+			index = i;
+			max_mw = mw;
+			max_mv = pps_max_mv;
+		}
+	}
+
+	if (index) {
+		pdo = port->source_caps[index];
+
+		port->pps_data.min_volt = pdo_pps_apdo_min_voltage(pdo);
+		port->pps_data.max_volt = pdo_pps_apdo_max_voltage(pdo);
+		port->pps_data.max_curr =
+			min(pdo_pps_apdo_max_current(pdo), port->max_snk_ma);
+		port->pps_data.out_volt =
+			min(pdo_pps_apdo_max_voltage(pdo), port->pps_data.out_volt);
+		port->pps_data.op_curr =
+			min(pdo_pps_apdo_max_current(pdo), port->pps_data.op_curr);
+	}
+
+	return index;
+}
+
 static int tcpm_pd_build_request(struct tcpm_port *port, u32 *rdo)
 {
 	unsigned int mv, ma, mw, flags;
@@ -1809,10 +2005,18 @@  static int tcpm_pd_build_request(struct tcpm_port *port, u32 *rdo)
 	pdo = port->source_caps[index];
 	type = pdo_type(pdo);
 
-	if (type == PDO_TYPE_FIXED)
+	switch (type) {
+	case PDO_TYPE_FIXED:
 		mv = pdo_fixed_voltage(pdo);
-	else
+		break;
+	case PDO_TYPE_BATT:
+	case PDO_TYPE_VAR:
 		mv = pdo_min_voltage(pdo);
+		break;
+	default:
+		tcpm_log(port, "Invalid PDO selected!");
+		return -EINVAL;
+	}
 
 	/* Select maximum available current within the board's power limit */
 	if (type == PDO_TYPE_BATT) {
@@ -1875,6 +2079,105 @@  static int tcpm_pd_send_request(struct tcpm_port *port)
 	msg.header = PD_HEADER_LE(PD_DATA_REQUEST,
 				  port->pwr_role,
 				  port->data_role,
+				  port->negotiated_rev,
+				  port->message_id, 1);
+	msg.payload[0] = cpu_to_le32(rdo);
+
+	return tcpm_pd_transmit(port, TCPC_TX_SOP, &msg);
+}
+
+static int tcpm_pd_build_pps_request(struct tcpm_port *port, u32 *rdo)
+{
+	unsigned int out_mv, op_ma, op_mw, min_mv, max_mv, max_ma, flags;
+	enum pd_pdo_type type;
+	int index;
+	u32 pdo;
+
+	index = tcpm_pd_select_pps_apdo(port);
+	if (!index)
+		return -EOPNOTSUPP;
+
+	pdo = port->source_caps[index];
+	type = pdo_type(pdo);
+
+	switch (type) {
+	case PDO_TYPE_APDO:
+		if (pdo_apdo_type(pdo) != APDO_TYPE_PPS) {
+			tcpm_log(port, "Invalid APDO selected!");
+			return -EINVAL;
+		}
+		min_mv = port->pps_data.min_volt;
+		max_mv = port->pps_data.max_volt;
+		max_ma = port->pps_data.max_curr;
+		out_mv = port->pps_data.out_volt;
+		op_ma = port->pps_data.op_curr;
+		break;
+	default:
+		tcpm_log(port, "Invalid PDO selected!");
+		return -EINVAL;
+	}
+
+	flags = RDO_USB_COMM | RDO_NO_SUSPEND;
+
+	op_mw = (op_ma * out_mv) / 1000;
+	if (op_mw < port->operating_snk_mw) {
+		/*
+		 * Try raising current to meet power needs. If that's not enough
+		 * then try upping the voltage. If that's still not enough
+		 * then we've obviously chosen a PPS APDO which really isn't
+		 * suitable so abandon ship.
+		 */
+		op_ma = (port->operating_snk_mw * 1000) / out_mv;
+		if ((port->operating_snk_mw * 1000) % out_mv)
+			++op_ma;
+		op_ma += RDO_PROG_CURR_MA_STEP - (op_ma % RDO_PROG_CURR_MA_STEP);
+
+		if (op_ma > max_ma) {
+			op_ma = max_ma;
+			out_mv = (port->operating_snk_mw * 1000) / op_ma;
+			if ((port->operating_snk_mw * 1000) % op_ma)
+				++out_mv;
+			out_mv += RDO_PROG_VOLT_MV_STEP -
+				  (out_mv % RDO_PROG_VOLT_MV_STEP);
+
+			if (out_mv > max_mv) {
+				tcpm_log(port, "Invalid PPS APDO selected!");
+				return -EINVAL;
+			}
+		}
+	}
+
+	tcpm_log(port, "cc=%d cc1=%d cc2=%d vbus=%d vconn=%s polarity=%d",
+		 port->cc_req, port->cc1, port->cc2, port->vbus_source,
+		 port->vconn_role == TYPEC_SOURCE ? "source" : "sink",
+		 port->polarity);
+
+	*rdo = RDO_PROG(index + 1, out_mv, op_ma, flags);
+
+	tcpm_log(port, "Requesting APDO %d: %u mV, %u mA",
+		 index, out_mv, op_ma);
+
+	port->pps_data.op_curr = op_ma;
+	port->pps_data.out_volt = out_mv;
+
+	return 0;
+}
+
+static int tcpm_pd_send_pps_request(struct tcpm_port *port)
+{
+	struct pd_message msg;
+	int ret;
+	u32 rdo;
+
+	ret = tcpm_pd_build_pps_request(port, &rdo);
+	if (ret < 0)
+		return ret;
+
+	memset(&msg, 0, sizeof(msg));
+	msg.header = PD_HEADER_LE(PD_DATA_REQUEST,
+				  port->pwr_role,
+				  port->data_role,
+				  port->negotiated_rev,
 				  port->message_id, 1);
 	msg.payload[0] = cpu_to_le32(rdo);
 
@@ -2060,6 +2363,7 @@  static void tcpm_reset_port(struct tcpm_port *port)
 	tcpm_typec_disconnect(port);
 	port->attached = false;
 	port->pd_capable = false;
+	port->pps_data.supported = false;
 
 	/*
 	 * First Rx ID should be 0; set this to a sentinel of -1 so that
@@ -2075,6 +2379,8 @@  static void tcpm_reset_port(struct tcpm_port *port)
 	tcpm_set_attached_state(port, false);
 	port->try_src_count = 0;
 	port->try_snk_count = 0;
+	port->supply_voltage = 0;
+	port->current_limit = 0;
 }
 
 static void tcpm_detach(struct tcpm_port *port)
@@ -2321,6 +2627,7 @@  static void run_state_machine(struct tcpm_port *port)
 		typec_set_pwr_opmode(port->typec_port, opmode);
 		port->pwr_opmode = TYPEC_PWR_MODE_USB;
 		port->caps_count = 0;
+		port->negotiated_rev = PD_MAX_REV;
 		port->message_id = 0;
 		port->rx_msgid = -1;
 		port->explicit_contract = false;
@@ -2381,6 +2688,7 @@  static void run_state_machine(struct tcpm_port *port)
 
 		tcpm_swap_complete(port, 0);
 		tcpm_typec_connect(port);
+
 		tcpm_check_send_discover(port);
 		/*
 		 * 6.3.5
@@ -2404,6 +2712,7 @@  static void run_state_machine(struct tcpm_port *port)
 	case SNK_UNATTACHED:
 		if (!port->non_pd_role_swap)
 			tcpm_swap_complete(port, -ENOTCONN);
+		tcpm_pps_complete(port, -ENOTCONN);
 		tcpm_snk_detach(port);
 		if (tcpm_start_drp_toggling(port)) {
 			tcpm_set_state(port, DRP_TOGGLING, 0);
@@ -2493,6 +2802,7 @@  static void run_state_machine(struct tcpm_port *port)
 					      port->cc2 : port->cc1);
 		typec_set_pwr_opmode(port->typec_port, opmode);
 		port->pwr_opmode = TYPEC_PWR_MODE_USB;
+		port->negotiated_rev = PD_MAX_REV;
 		port->message_id = 0;
 		port->rx_msgid = -1;
 		port->explicit_contract = false;
@@ -2563,6 +2873,24 @@  static void run_state_machine(struct tcpm_port *port)
 					    PD_T_SENDER_RESPONSE);
 		}
 		break;
+	case SNK_NEGOTIATE_PPS_CAPABILITIES:
+		ret = tcpm_pd_send_pps_request(port);
+		if (ret < 0) {
+			port->pps_status = ret;
+			/*
+			 * If this was called due to updates to sink
+			 * capabilities, and pps is no longer valid, we should
+			 * safely fall back to a standard PDO.
+			 */
+			if (port->update_sink_caps)
+				tcpm_set_state(port, SNK_NEGOTIATE_CAPABILITIES, 0);
+			else
+				tcpm_set_state(port, SNK_READY, 0);
+		} else {
+			tcpm_set_state_cond(port, hard_reset_state(port),
+					    PD_T_SENDER_RESPONSE);
+		}
+		break;
 	case SNK_TRANSITION_SINK:
 	case SNK_TRANSITION_SINK_VBUS:
 		tcpm_set_state(port, hard_reset_state(port),
@@ -2570,6 +2898,7 @@  static void run_state_machine(struct tcpm_port *port)
 		break;
 	case SNK_READY:
 		port->try_snk_count = 0;
+		port->update_sink_caps = false;
 		if (port->explicit_contract) {
 			typec_set_pwr_opmode(port->typec_port,
 					     TYPEC_PWR_MODE_PD);
@@ -2579,6 +2908,8 @@  static void run_state_machine(struct tcpm_port *port)
 		tcpm_swap_complete(port, 0);
 		tcpm_typec_connect(port);
 		tcpm_check_send_discover(port);
+		tcpm_pps_complete(port, port->pps_status);
+
 		break;
 
 	/* Accessory states */
@@ -2625,6 +2956,7 @@  static void run_state_machine(struct tcpm_port *port)
 		tcpm_set_state(port, SRC_UNATTACHED, PD_T_PS_SOURCE_ON);
 		break;
 	case SNK_HARD_RESET_SINK_OFF:
+		memset(&port->pps_data, 0, sizeof(port->pps_data));
 		tcpm_set_vconn(port, false);
 		tcpm_set_charge(port, false);
 		tcpm_set_roles(port, false, TYPEC_SINK, TYPEC_DEVICE);
@@ -2845,6 +3177,7 @@  static void run_state_machine(struct tcpm_port *port)
 		break;
 	case ERROR_RECOVERY:
 		tcpm_swap_complete(port, -EPROTO);
+		tcpm_pps_complete(port, -EPROTO);
 		tcpm_set_state(port, PORT_RESET, 0);
 		break;
 	case PORT_RESET:
@@ -3427,6 +3760,162 @@  static int tcpm_try_role(const struct typec_capability *cap, int role)
 	return ret;
 }
 
+static int tcpm_pps_set_op_curr(struct tcpm_port *port, u16 op_curr)
+{
+	unsigned int target_mw;
+	int ret;
+
+	mutex_lock(&port->swap_lock);
+	mutex_lock(&port->lock);
+
+	if (!port->pps_data.active) {
+		ret = -EOPNOTSUPP;
+		goto port_unlock;
+	}
+
+	if (port->state != SNK_READY) {
+		ret = -EAGAIN;
+		goto port_unlock;
+	}
+
+	if (op_curr > port->pps_data.max_curr) {
+		ret = -EINVAL;
+		goto port_unlock;
+	}
+
+	target_mw = (op_curr * port->pps_data.out_volt) / 1000;
+	if (target_mw < port->operating_snk_mw) {
+		ret = -EINVAL;
+		goto port_unlock;
+	}
+
+	reinit_completion(&port->pps_complete);
+	port->pps_data.op_curr = op_curr;
+	port->pps_status = 0;
+	port->pps_pending = true;
+	tcpm_set_state(port, SNK_NEGOTIATE_PPS_CAPABILITIES, 0);
+	mutex_unlock(&port->lock);
+
+	if (!wait_for_completion_timeout(&port->pps_complete,
+				msecs_to_jiffies(PD_PPS_CTRL_TIMEOUT)))
+		ret = -ETIMEDOUT;
+	else
+		ret = port->pps_status;
+
+	goto swap_unlock;
+
+port_unlock:
+	mutex_unlock(&port->lock);
+swap_unlock:
+	mutex_unlock(&port->swap_lock);
+
+	return ret;
+}
+
+static int tcpm_pps_set_out_volt(struct tcpm_port *port, u16 out_volt)
+{
+	unsigned int target_mw;
+	int ret;
+
+	mutex_lock(&port->swap_lock);
+	mutex_lock(&port->lock);
+
+	if (!port->pps_data.active) {
+		ret = -EOPNOTSUPP;
+		goto port_unlock;
+	}
+
+	if (port->state != SNK_READY) {
+		ret = -EAGAIN;
+		goto port_unlock;
+	}
+
+	if (out_volt < port->pps_data.min_volt ||
+	    out_volt > port->pps_data.max_volt) {
+		ret = -EINVAL;
+		goto port_unlock;
+	}
+
+	target_mw = (port->pps_data.op_curr * out_volt) / 1000;
+	if (target_mw < port->operating_snk_mw) {
+		ret = -EINVAL;
+		goto port_unlock;
+	}
+
+	reinit_completion(&port->pps_complete);
+	port->pps_data.out_volt = out_volt;
+	port->pps_status = 0;
+	port->pps_pending = true;
+	tcpm_set_state(port, SNK_NEGOTIATE_PPS_CAPABILITIES, 0);
+	mutex_unlock(&port->lock);
+
+	if (!wait_for_completion_timeout(&port->pps_complete,
+				msecs_to_jiffies(PD_PPS_CTRL_TIMEOUT)))
+		ret = -ETIMEDOUT;
+	else
+		ret = port->pps_status;
+
+	goto swap_unlock;
+
+port_unlock:
+	mutex_unlock(&port->lock);
+swap_unlock:
+	mutex_unlock(&port->swap_lock);
+
+	return ret;
+}
+
+static int tcpm_pps_activate(struct tcpm_port *port, bool activate)
+{
+	int ret = 0;
+
+	mutex_lock(&port->swap_lock);
+	mutex_lock(&port->lock);
+
+	if (!port->pps_data.supported) {
+		ret = -EOPNOTSUPP;
+		goto port_unlock;
+	}
+
+	/* Trying to deactivate PPS when already deactivated so just bail */
+	if (!port->pps_data.active && !activate)
+		goto port_unlock;
+
+	if (port->state != SNK_READY) {
+		ret = -EAGAIN;
+		goto port_unlock;
+	}
+
+	reinit_completion(&port->pps_complete);
+	port->pps_status = 0;
+	port->pps_pending = true;
+
+	/* Trigger PPS request or move back to standard PDO contract */
+	if (activate) {
+		port->pps_data.out_volt = port->supply_voltage;
+		port->pps_data.op_curr = port->current_limit;
+		tcpm_set_state(port, SNK_NEGOTIATE_PPS_CAPABILITIES, 0);
+	} else {
+		tcpm_set_state(port, SNK_NEGOTIATE_CAPABILITIES, 0);
+	}
+	mutex_unlock(&port->lock);
+
+	if (!wait_for_completion_timeout(&port->pps_complete,
+				msecs_to_jiffies(PD_PPS_CTRL_TIMEOUT)))
+		ret = -ETIMEDOUT;
+	else
+		ret = port->pps_status;
+
+	goto swap_unlock;
+
+port_unlock:
+	mutex_unlock(&port->lock);
+swap_unlock:
+	mutex_unlock(&port->swap_lock);
+
+	return ret;
+}
+
 static void tcpm_init(struct tcpm_port *port)
 {
 	enum typec_cc_status cc1, cc2;
@@ -3566,13 +4055,18 @@  int tcpm_update_sink_capabilities(struct tcpm_port *port, const u32 *pdo,
 	port->max_snk_ma = max_snk_ma;
 	port->max_snk_mw = max_snk_mw;
 	port->operating_snk_mw = operating_snk_mw;
+	port->update_sink_caps = true;
 
 	switch (port->state) {
 	case SNK_NEGOTIATE_CAPABILITIES:
+	case SNK_NEGOTIATE_PPS_CAPABILITIES:
 	case SNK_READY:
 	case SNK_TRANSITION_SINK:
 	case SNK_TRANSITION_SINK_VBUS:
-		tcpm_set_state(port, SNK_NEGOTIATE_CAPABILITIES, 0);
+		if (port->pps_data.active)
+			tcpm_set_state(port, SNK_NEGOTIATE_PPS_CAPABILITIES, 0);
+		else
+			tcpm_set_state(port, SNK_NEGOTIATE_CAPABILITIES, 0);
 		break;
 	default:
 		break;
@@ -3614,6 +4108,7 @@  struct tcpm_port *tcpm_register_port(struct device *dev, struct tcpc_dev *tcpc)
 
 	init_completion(&port->tx_complete);
 	init_completion(&port->swap_complete);
+	init_completion(&port->pps_complete);
 	tcpm_debugfs_init(port);
 
 	if (tcpm_validate_caps(port, tcpc->config->src_pdo,
@@ -3642,7 +4137,7 @@  struct tcpm_port *tcpm_register_port(struct device *dev, struct tcpc_dev *tcpc)
 	port->typec_caps.prefer_role = tcpc->config->default_role;
 	port->typec_caps.type = tcpc->config->type;
 	port->typec_caps.revision = 0x0120;	/* Type-C spec release 1.2 */
-	port->typec_caps.pd_revision = 0x0200;	/* USB-PD spec release 2.0 */
+	port->typec_caps.pd_revision = 0x0300;	/* USB-PD spec release 3.0 */
 	port->typec_caps.dr_set = tcpm_dr_set;
 	port->typec_caps.pr_set = tcpm_pr_set;
 	port->typec_caps.vconn_set = tcpm_vconn_set;
diff --git a/include/linux/usb/pd.h b/include/linux/usb/pd.h
index ff359bdf..09b570f 100644
--- a/include/linux/usb/pd.h
+++ b/include/linux/usb/pd.h
@@ -103,8 +103,8 @@  enum pd_ext_msg_type {
 	 (((cnt) & PD_HEADER_CNT_MASK) << PD_HEADER_CNT_SHIFT) |	\
 	 ((ext_hdr) ? PD_HEADER_EXT_HDR : 0))
 
-#define PD_HEADER_LE(type, pwr, data, id, cnt) \
-	cpu_to_le16(PD_HEADER((type), (pwr), (data), PD_REV20, (id), (cnt), (0)))
+#define PD_HEADER_LE(type, pwr, data, rev, id, cnt) \
+	cpu_to_le16(PD_HEADER((type), (pwr), (data), (rev), (id), (cnt), (0)))
 
 static inline unsigned int pd_header_cnt(u16 header)
 {
diff --git a/include/linux/usb/tcpm.h b/include/linux/usb/tcpm.h
index ca1c0b5..220ecc0 100644
--- a/include/linux/usb/tcpm.h
+++ b/include/linux/usb/tcpm.h
@@ -36,6 +36,7 @@  enum typec_cc_polarity {
 /* Time to wait for TCPC to complete transmit */
 #define PD_T_TCPC_TX_TIMEOUT	100		/* in ms	*/
 #define PD_ROLE_SWAP_TIMEOUT	(MSEC_PER_SEC * 10)
+#define PD_PPS_CTRL_TIMEOUT	(MSEC_PER_SEC * 10)
 
 enum tcpm_transmit_status {
 	TCPC_TX_SUCCESS = 0,