Enable the PD irq and decode the contract.

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

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;
-- 
2.25.1

Reply via email to