diff mbox series

[4/4] usb: typec: tps6598x: Add the power delivery irq

Message ID 20200914164639.1487650-5-angus@akkea.ca
State New
Headers show
Series RFC: USB C extcon patchset for the tps6598x | expand

Commit Message

Angus Ainslie Sept. 14, 2020, 4:46 p.m. UTC
Enable the PD irq and decode the contract.

Signed-off-by: Angus Ainslie <angus@akkea.ca>
---
 drivers/usb/typec/tps6598x.c | 386 ++++++++++++++++++++++++++++++++---
 1 file changed, 360 insertions(+), 26 deletions(-)
diff mbox series

Patch

diff --git a/drivers/usb/typec/tps6598x.c b/drivers/usb/typec/tps6598x.c
index d5aa58c9da0a..2eb883ff822c 100644
--- a/drivers/usb/typec/tps6598x.c
+++ b/drivers/usb/typec/tps6598x.c
@@ -30,11 +30,19 @@ 
 #define TPS_REG_STATUS			0x1a
 #define TPS_REG_SYSTEM_CONF		0x28
 #define TPS_REG_CTRL_CONF		0x29
+#define TPS_REG_ACTIVE_CONTRACT_PDO	0x34
+#define TPS_REG_ACTIVE_CONTRACT_RDO	0x35
 #define TPS_REG_POWER_STATUS		0x3f
 #define TPS_REG_RX_IDENTITY_SOP		0x48
+#define TPS_REG_DATA_STATUS		0x5f
 
 /* TPS_REG_INT_* bits */
 #define TPS_REG_INT_PLUG_EVENT		BIT(3)
+#define TPS_REG_INT_NEW_CONTRACT_SNK	BIT(12)
+#define TPS_REG_INT_PWR_STATUS_UPDATE	BIT(24)
+#define TPS_REG_INT_DATA_STATUS_UPDATE	BIT(25)
+#define TPS_REG_INT_STATUS_UPDATE	BIT(26)
+#define TPS_REG_INT_PD_STATUS_UPDATE	BIT(27)
 
 /* TPS_REG_STATUS bits */
 #define TPS_STATUS_PLUG_PRESENT		BIT(0)
@@ -57,8 +65,16 @@  enum {
 };
 
 /* TPS_REG_POWER_STATUS bits */
+#define TPS_POWER_STATUS_CONNECTION	BIT(0)
 #define TPS_POWER_STATUS_SOURCESINK	BIT(1)
+#define TPS_POWER_STATUS_BC12_CON	BIT(4)
+#define TPS_POWER_STATUS_BC_SDP		0
+#define TPS_POWER_STATUS_BC_CDP		2
+#define TPS_POWER_STATUS_BC_DCP		3
+
+#define TPS_POWER_STATUS_BCOPMODE(p)	(((p) & GENMASK(6, 5)) >> 5)
 #define TPS_POWER_STATUS_PWROPMODE(p)	(((p) & GENMASK(3, 2)) >> 2)
+#define TPS_POWER_STATUS_SRC(p)		(!(((p) & BIT(1)) >> 1))
 
 /* TPS_REG_RX_IDENTITY_SOP */
 struct tps6598x_rx_identity_reg {
@@ -217,21 +233,237 @@  static void tps6598x_set_data_role(struct tps6598x *tps,
 }
 
 #ifdef CONFIG_EXTCON
-static void tps6589x_set_extcon_state(struct tps6598x *tps,
-				      u32 status, u16 pwr_status, bool state)
+
+/*
+ *   the PDO decoding comes from here
+ *   https://www.ti.com/lit/an/slva842/slva842.pdf
+ */
+
+struct contract_terms {
+	u8 type;
+	bool dr_power;
+	bool dr_data;
+	bool higher_cap;
+	bool ext_powered;
+	bool usb_comms;
+	int max_voltage;
+	int max_current;
+	int max_power;
+};
+
+#define PDO_CONTRACT_TYPE(x)	(x >> 30 & 0x3)
+#define PDO_CONTRACT_FIXED	0
+#define PDO_CONTRACT_BATTERY	1
+#define PDO_CONTRACT_VARIABLE	2
+
+#define PDO_CONTRACT_DR_POWER		BIT(29)
+#define PDO_CONTRACT_HIGHER_CAP		BIT(28)
+#define PDO_CONTRACT_EXTERNAL_PWR	BIT(27)
+#define PDO_CONTRACT_USB_COMMS		BIT(26)
+#define PDO_CONTRACT_DR_DATA		BIT(25)
+
+#define PDO_CONTRACT_VOLTAGE(x)		((x >> 10 & 0x3ff) * 50)
+#define PDO_CONTRACT_MIN_VOLTAGE(x)	PDO_CONTRACT_VOLTAGE(x)
+#define PDO_CONTRACT_MAX_VOLTAGE(x)	((x >> 20 & 0x3ff) * 50)
+#define PDO_CONTRACT_CURRENT(x)		((x & 0x3ff) * 10)
+#define PDO_CONTRACT_POWER(x)		((x & 0x3ff) * 250)
+
+static int tps6598x_decode_pdo_contract(struct tps6598x *tps, u32 contract,
+					struct contract_terms *terms)
+{
+	int min_voltage = 5000; // mV
+
+	memset(terms, 0, sizeof(struct contract_terms));
+
+	dev_dbg(tps->dev, "%s 0x%x\n", __func__, contract);
+
+	switch (PDO_CONTRACT_TYPE(contract)) {
+	case PDO_CONTRACT_FIXED:
+		terms->type = PDO_CONTRACT_FIXED;
+		if (contract & PDO_CONTRACT_DR_POWER) {
+			dev_dbg(tps->dev, "Dual role power\n");
+			terms->dr_power = true;
+		}
+		if (contract & PDO_CONTRACT_DR_DATA) {
+			dev_dbg(tps->dev, "Dual role data\n");
+			terms->dr_data = true;
+		}
+		if (contract & PDO_CONTRACT_HIGHER_CAP) {
+			dev_dbg(tps->dev, "Higher capbility\n");
+			terms->higher_cap = true;
+		}
+		if (contract & PDO_CONTRACT_EXTERNAL_PWR) {
+			dev_dbg(tps->dev, "Externally powered\n");
+			terms->ext_powered = true;
+		}
+		if (contract & PDO_CONTRACT_USB_COMMS) {
+			dev_dbg(tps->dev, "USB communications capable\n");
+			terms->usb_comms = true;
+		}
+		terms->max_voltage = PDO_CONTRACT_VOLTAGE(contract);
+		terms->max_current = PDO_CONTRACT_CURRENT(contract);
+		terms->max_power = (terms->max_voltage * terms->max_current) / 1000;
+		dev_dbg(tps->dev, "Fixed contract uVMIN:uVMAX:uA:uW %d:%d:%d:%d\n",
+			min_voltage, terms->max_voltage, terms->max_current,
+			terms->max_power);
+		break;
+	case PDO_CONTRACT_BATTERY:
+		min_voltage = PDO_CONTRACT_MIN_VOLTAGE(contract);
+		terms->max_voltage = PDO_CONTRACT_MAX_VOLTAGE(contract);
+		terms->max_power = PDO_CONTRACT_POWER(contract);
+		terms->max_current = terms->max_power / (terms->max_voltage / 1000);
+		dev_dbg(tps->dev, "Battery contract uVMIN:uVMAX:uA:uW %d:%d:%d:%d\n",
+			min_voltage, terms->max_voltage, terms->max_current,
+			terms->max_power);
+		break;
+	case PDO_CONTRACT_VARIABLE:
+		min_voltage = PDO_CONTRACT_MIN_VOLTAGE(contract);
+		terms->max_voltage = PDO_CONTRACT_MAX_VOLTAGE(contract);
+		terms->max_current = PDO_CONTRACT_CURRENT(contract);
+		terms->max_power = (terms->max_voltage * terms->max_current) / 1000;
+		dev_dbg(tps->dev, "Variable contract uVMIN:uVMAX:uA:uW %d:%d:%d:%d\n",
+			min_voltage, terms->max_voltage, terms->max_current,
+			terms->max_power);
+		break;
+	default:
+		dev_warn(tps->dev, "Unknown supply\n");
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static int tps6598x_pdo_contract(struct tps6598x *tps, u32 contract)
 {
 	union extcon_property_value val;
-	int max_current;
+	struct contract_terms terms;
 
-	if (TPS_STATUS_DATAROLE(status)) {
-		extcon_set_state(tps->edev, EXTCON_USB, false);
-		extcon_set_state(tps->edev, EXTCON_USB_HOST, state);
+	tps6598x_decode_pdo_contract(tps, contract, &terms);
+
+	val.intval = terms.max_current;
+	extcon_set_property(tps->edev, EXTCON_USB,
+			    EXTCON_PROP_USB_VBUS_CURRENT,
+			    val);
+	extcon_set_property(tps->edev, EXTCON_USB_HOST,
+			    EXTCON_PROP_USB_VBUS_CURRENT,
+			    val);
+
+	val.intval = terms.max_voltage;
+	extcon_set_property(tps->edev, EXTCON_USB,
+			    EXTCON_PROP_USB_VBUS_VOLTAGE,
+			    val);
+	extcon_set_property(tps->edev, EXTCON_USB_HOST,
+			    EXTCON_PROP_USB_VBUS_VOLTAGE,
+			    val);
+
+	return terms.max_current;
+}
+
+#define RDO_CONTRACT_OBJ_POSITION		BIT(29)
+#define RDO_CONTRACT_CAPABILITY_MISMATCH	BIT(27)
+#define RDO_CONTRACT_USB_COMMS			BIT(26)
+#define RDO_CONTRACT_NO_SUSPEND			BIT(25)
+
+#define RDO_CONTRACT_CURRENT(x)		((x >> 10 & 0x3ff) * 10)
+#define RDO_CONTRACT_POWER(x)		((x >> 10 & 0x3ff) * 250)
+#define RDO_CONTRACT_MAX_CURRENT(x)	((x & 0x3ff) * 10)
+#define RDO_CONTRACT_MAX_POWER(x)	((x & 0x3ff) * 250)
+
+static int tps6598x_decode_rdo_contract(struct tps6598x *tps, u32 contract, bool battery,
+					struct contract_terms *terms)
+{
+	memset(terms, 0, sizeof(struct contract_terms));
+
+	if (contract & RDO_CONTRACT_OBJ_POSITION)
+		dev_dbg(tps->dev, "Object position\n");
+	if (contract & RDO_CONTRACT_CAPABILITY_MISMATCH)
+		dev_dbg(tps->dev, "Capbility mismatch\n");
+	if (contract & RDO_CONTRACT_USB_COMMS)
+		dev_dbg(tps->dev, "USB communications capable\n");
+	if (contract & RDO_CONTRACT_NO_SUSPEND)
+		dev_dbg(tps->dev, "No USB suspend\n");
+	if (battery) {
+		terms->max_power = RDO_CONTRACT_MAX_POWER(contract);
+		dev_dbg(tps->dev, "Power %d max power %d\n", RDO_CONTRACT_POWER(contract),
+			terms->max_power);
 	} else {
-		extcon_set_state(tps->edev, EXTCON_USB, state);
-		extcon_set_state(tps->edev, EXTCON_USB_HOST, false);
+		terms->max_current = RDO_CONTRACT_MAX_CURRENT(contract); /* mA */
+		dev_dbg(tps->dev, "Current %d max current %d\n", RDO_CONTRACT_CURRENT(contract),
+			terms->max_current);
 	}
 
-	val.intval = TPS_STATUS_PORTROLE(status);
+	return 0;
+}
+
+static int tps6598x_rdo_contract(struct tps6598x *tps, u32 contract, bool battery)
+{
+	union extcon_property_value val;
+	struct contract_terms terms;
+
+	if (battery)
+		dev_dbg(tps->dev, "Battery supply\n");
+
+	tps6598x_decode_rdo_contract(tps, contract, battery, &terms);
+
+	if (terms.max_current > 0) {
+		val.intval = terms.max_current;
+		extcon_set_property(tps->edev, EXTCON_USB,
+				    EXTCON_PROP_USB_VBUS_CURRENT,
+				    val);
+		extcon_set_property(tps->edev, EXTCON_USB_HOST,
+				    EXTCON_PROP_USB_VBUS_CURRENT,
+				    val);
+
+	}
+
+	return terms.max_current;
+}
+
+static int tps6598x_set_extcon_pd_state(struct tps6598x *tps, bool state)
+{
+	u32 pdo_contract, rdo_contract;
+	int ret, max_current;
+
+	ret = tps6598x_read32(tps,
+			      TPS_REG_ACTIVE_CONTRACT_RDO,
+			      &rdo_contract);
+	if (ret) {
+		dev_dbg(tps->dev,
+			"failed to read RDO contract - try PDO contract\n");
+		rdo_contract = 0;
+	}
+
+	if (rdo_contract != 0) {
+		max_current = tps6598x_rdo_contract(tps, rdo_contract, false);
+	} else {
+		ret = tps6598x_read32(tps,
+				      TPS_REG_ACTIVE_CONTRACT_PDO,
+				      &pdo_contract);
+		max_current = tps6598x_pdo_contract(tps, pdo_contract);
+		if (ret || pdo_contract == 0)
+			dev_err(tps->dev, "failed to read PDO contract\n");
+	}
+
+	extcon_set_state(tps->edev, EXTCON_CHG_USB_PD, state);
+	extcon_sync(tps->edev, EXTCON_CHG_USB_PD);
+
+	return max_current;
+}
+
+static void tps6598x_set_extcon_power_state(struct tps6598x *tps, u32 status,
+					    u16 pwr_status)
+{
+	int max_current = 100;
+	union extcon_property_value val;
+	u8 connected;
+
+	/* USB C power source */
+	val.intval = TPS_POWER_STATUS_SRC(pwr_status) &&
+		TPS_STATUS_PORTROLE(status);
+
+	dev_dbg(tps->dev, "Power status 0x%x Source %d\n", pwr_status,
+		val.intval);
+
 	extcon_set_property(tps->edev, EXTCON_USB_HOST,
 			    EXTCON_PROP_USB_VBUS_SRC,
 			    val);
@@ -240,28 +472,55 @@  static void tps6589x_set_extcon_state(struct tps6598x *tps,
 			    EXTCON_PROP_USB_VBUS_SRC,
 			    val);
 
-	extcon_set_state(tps->edev, EXTCON_CHG_USB_SDP, 0);
-	extcon_set_state(tps->edev, EXTCON_CHG_USB_DCP, 0);
-	extcon_set_state(tps->edev, EXTCON_CHG_USB_CDP, 0);
-	extcon_set_state(tps->edev, EXTCON_CHG_USB_SLOW, 0);
-	extcon_set_state(tps->edev, EXTCON_CHG_USB_FAST, 0);
+	connected = pwr_status & TPS_POWER_STATUS_CONNECTION;
+
+	if (pwr_status & TPS_POWER_STATUS_BC12_CON) {
+		switch (TPS_POWER_STATUS_BCOPMODE(pwr_status)) {
+		case TPS_POWER_STATUS_BC_SDP:
+			/* TODO: detect USB3 900mA */
+			extcon_set_state(tps->edev,
+					 EXTCON_CHG_USB_SLOW, connected);
+			dev_dbg(tps->dev, "BC1.2 SDP detected\n");
+			extcon_set_state(tps->edev,
+					 EXTCON_CHG_USB_SDP, connected);
+			break;
+		case TPS_POWER_STATUS_BC_CDP:
+			dev_dbg(tps->dev, "BC1.2 CDP detected\n");
+			extcon_set_state(tps->edev,
+					 EXTCON_CHG_USB_CDP, connected);
+			break;
+		case TPS_POWER_STATUS_BC_DCP:
+			dev_dbg(tps->dev, "BC1.2 DCP detected\n");
+			extcon_set_state(tps->edev,
+					 EXTCON_CHG_USB_DCP, connected);
+			break;
+		default:
+			dev_dbg(tps->dev, "BC1.2 unknown - reserved\n");
+			break;
+
+		}
+	}
 
 	switch (TPS_POWER_STATUS_PWROPMODE(pwr_status)) {
 	case TYPEC_PWR_MODE_USB:
-		max_current = val.intval = 500;
-		extcon_set_state(tps->edev, EXTCON_CHG_USB_SDP, state);
-		extcon_set_state(tps->edev, EXTCON_CHG_USB_SLOW, state);
+		if (max_current < 500)
+			max_current = 500;
+		/* TODO: detect UBS3 900mA */
+		extcon_set_state(tps->edev, EXTCON_CHG_USB_SLOW, connected);
+		extcon_set_state(tps->edev, EXTCON_CHG_USB_SDP, connected);
 		break;
 	case TYPEC_PWR_MODE_1_5A:
-		max_current = val.intval = 1500;
-		extcon_set_state(tps->edev, EXTCON_CHG_USB_CDP, state);
+		if (max_current < 1500)
+			max_current = 1500;
+		extcon_set_state(tps->edev, EXTCON_CHG_USB_CDP, connected);
 		break;
 	case TYPEC_PWR_MODE_3_0A:
-		max_current = val.intval = 3000;
-		extcon_set_state(tps->edev, EXTCON_CHG_USB_CDP, state);
+		if (max_current < 3000)
+			max_current = 3000;
 		break;
 	case TYPEC_PWR_MODE_PD:
-		max_current = 500;
+		extcon_set_state(tps->edev, EXTCON_CHG_USB_PD, connected);
+		max_current = tps6598x_set_extcon_pd_state(tps, connected);
 		break;
 	}
 
@@ -272,6 +531,38 @@  static void tps6589x_set_extcon_state(struct tps6598x *tps,
 	extcon_set_property(tps->edev, EXTCON_USB_HOST,
 			    EXTCON_PROP_USB_VBUS_CURRENT,
 			    val);
+}
+
+static void tps6598x_set_extcon_state(struct tps6598x *tps,
+				      u32 status, u16 pwr_status, bool state)
+{
+	union extcon_property_value val;
+
+	if (TPS_STATUS_DATAROLE(status)) {
+		extcon_set_state(tps->edev, EXTCON_USB, false);
+		extcon_set_state(tps->edev, EXTCON_USB_HOST, state);
+	} else {
+		extcon_set_state(tps->edev, EXTCON_USB, state);
+		extcon_set_state(tps->edev, EXTCON_USB_HOST, false);
+	}
+
+	val.intval = TPS_STATUS_PORTROLE(status);
+	extcon_set_property(tps->edev, EXTCON_USB_HOST,
+			    EXTCON_PROP_USB_VBUS,
+			    val);
+
+	extcon_set_property(tps->edev, EXTCON_USB,
+			    EXTCON_PROP_USB_VBUS,
+			    val);
+
+	extcon_set_state(tps->edev, EXTCON_CHG_USB_SDP, 0);
+	extcon_set_state(tps->edev, EXTCON_CHG_USB_DCP, 0);
+	extcon_set_state(tps->edev, EXTCON_CHG_USB_CDP, 0);
+	extcon_set_state(tps->edev, EXTCON_CHG_USB_SLOW, 0);
+	extcon_set_state(tps->edev, EXTCON_CHG_USB_FAST, 0);
+	extcon_set_state(tps->edev, EXTCON_CHG_USB_PD, 0);
+
+	tps6598x_set_extcon_power_state(tps, status, pwr_status);
 
 	extcon_sync(tps->edev, EXTCON_CHG_USB_SDP);
 	extcon_sync(tps->edev, EXTCON_CHG_USB_CDP);
@@ -323,9 +614,9 @@  static int tps6598x_connect(struct tps6598x *tps, u32 status)
 	if (desc.identity)
 		typec_partner_set_identity(tps->partner);
 
-#ifdef CONFIG_EXTCON
-	tps6598x_set_extcon_state(tps, status, pwr_status, true);
-#endif
+	dev_dbg(tps->dev, "%s: mode 0x%x pwr_role %d vconn_role %d data_role %d\n",
+		__func__, mode, TPS_STATUS_PORTROLE(status),
+		TPS_STATUS_VCONN(status), TPS_STATUS_DATAROLE(status));
 
 	return 0;
 }
@@ -494,7 +785,12 @@  static irqreturn_t tps6598x_interrupt(int irq, void *data)
 	u64 event1;
 	u64 event2;
 	u32 status;
+	u32 data_status;
+	u16 power_status;
 	int ret;
+#ifdef CONFIG_EXTCON
+	bool connected;
+#endif
 
 	mutex_lock(&tps->lock);
 
@@ -514,8 +810,20 @@  static irqreturn_t tps6598x_interrupt(int irq, void *data)
 		goto err_clear_ints;
 	}
 
+	connected = status & TPS_STATUS_PLUG_PRESENT;
+	ret = tps6598x_read16(tps, TPS_REG_POWER_STATUS, &power_status);
+	if (ret < 0)
+		goto err_clear_ints;
+
+	ret = tps6598x_read32(tps, TPS_REG_DATA_STATUS, &data_status);
+	if (ret < 0)
+		goto err_clear_ints;
+
+	dev_dbg(tps->dev, "Status: 0x%x data: 0x%x power: 0x%x", status,
+		data_status, power_status);
+
 	/* Handle plug insert or removal */
-	if ((event1 | event2) & TPS_REG_INT_PLUG_EVENT) {
+	if ((event1 | event2) & (TPS_REG_INT_PLUG_EVENT)) {
 		if (status & TPS_STATUS_PLUG_PRESENT) {
 			ret = tps6598x_connect(tps, status);
 			if (ret)
@@ -526,6 +834,19 @@  static irqreturn_t tps6598x_interrupt(int irq, void *data)
 		}
 	}
 
+#ifdef CONFIG_EXTCON
+	/* updated power or data status register */
+	/* power bits sometimes change without triggering an interrupt */
+	if ((event1 | event2) & (TPS_REG_INT_STATUS_UPDATE |
+				 TPS_REG_INT_PWR_STATUS_UPDATE |
+				 TPS_REG_INT_DATA_STATUS_UPDATE |
+				 TPS_REG_INT_NEW_CONTRACT_SNK |
+				 TPS_REG_INT_PD_STATUS_UPDATE)) {
+		dev_dbg(tps->dev, "%s: update extcon\n", __func__);
+		tps6598x_set_extcon_state(tps, status, power_status, connected);
+	}
+#endif
+
 err_clear_ints:
 	tps6598x_write64(tps, TPS_REG_INT_CLEAR1, event1);
 	tps6598x_write64(tps, TPS_REG_INT_CLEAR2, event2);
@@ -591,6 +912,7 @@  static int tps6598x_probe(struct i2c_client *client)
 	u32 conf;
 	u32 vid;
 	int ret;
+	u64 mask;
 
 	tps = devm_kzalloc(&client->dev, sizeof(*tps), GFP_KERNEL);
 	if (!tps)
@@ -735,6 +1057,18 @@  static int tps6598x_probe(struct i2c_client *client)
 		goto err_role_put;
 	}
 
+	mask = TPS_REG_INT_NEW_CONTRACT_SNK
+		| TPS_REG_INT_PLUG_EVENT
+		| TPS_REG_INT_PWR_STATUS_UPDATE
+		| TPS_REG_INT_DATA_STATUS_UPDATE
+		| TPS_REG_INT_STATUS_UPDATE
+		| TPS_REG_INT_PD_STATUS_UPDATE;
+
+	ret = tps6598x_write64(tps, TPS_REG_INT_MASK1, mask);
+	ret |= tps6598x_write64(tps, TPS_REG_INT_MASK2, mask);
+	if (ret < 0)
+		dev_err(&client->dev, "failed to set irq mask %d\n", ret);
+
 	i2c_set_clientdata(client, tps);
 
 	return 0;